Added files

This commit is contained in:
Ruben Ramirez 2025-01-15 10:49:39 -06:00
parent e0e4433345
commit cc6dc646b2
14 changed files with 6248 additions and 0 deletions

19
composer.json Normal file
View file

@ -0,0 +1,19 @@
{
"require": {
"guzzlehttp/guzzle": "^7.9",
"symfony/mailer": "^7.2",
"symfony/http-client": "^7.2",
"phpseclib/phpseclib": "^3.0",
"openai-php/client": "^0.10.3",
"php-imap/php-imap": "^5.0",
"symfony/mime": "^7.2"
},
"config": {
"allow-plugins": {
"php-http/discovery": true
}
},
"require-dev": {
"phpunit/phpunit": "^11.5"
}
}

3989
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,432 @@
<?php
class RL_MailWarmer_Conversation_Handler {
/**
* Process conversations starting within the next 24 hours.
*
* This function finds conversations with a status of "new" that are scheduled to start within the next 24 hours,
* generates them, and updates their status to "generated." Limits to 50 conversations per run.
*/
public static function process_upcoming_conversations() {
log_to_file("process_upcoming_conversations - Running!");
global $wpdb;
$conversation_table = $wpdb->prefix . 'rl_mailwarmer_conversations';
// $current_time = current_time('mysql');
$current_time = date('Y-m-d H:i:s', strtotime('-24 hours'));
$future_time = date('Y-m-d H:i:s', strtotime('+24 hours'));
// Fetch up to 50 conversations starting within the next 24 hours
$conversations = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM $conversation_table WHERE status = %s AND first_message_timestamp BETWEEN %s AND %s LIMIT 1",
'new',
$current_time,
$future_time
),
ARRAY_A
);
// log_to_file("process_upcoming_conversations - Conversations: ", $conversations);
foreach ($conversations as $conversation) {
// log_to_file("process_upcoming_conversations - Conversation: ", $conversation);
try {
// Generate conversation content using AI
// $ai_response = self::fetch_conversation_from_ai($conversation);
$prompt = $conversation['prompt'];
// log_to_file("process_upcoming_conversations - AI prompt: ", $prompt);
// $ai_response = RL_MailWarmer_Campaign_Helper::clean_ai_response(RL_MailWarmer_Campaign_Helper::generate_ai_conversation($prompt));
// $ai_response = RL_MailWarmer_Campaign_Helper::generate_ai_conversation($prompt);
$ai_response = get_post_meta($conversation['campaign_id'], 'last_ai_response', true);
// log_to_file("process_upcoming_conversations - AI Response: ", $ai_response);
if (is_wp_error($ai_response)) {
return $ai_response; // Handle AI generation failure gracefully
}
$updated_conversation_steps = self::merge_timeline_with_ai_response($conversation['conversation_steps'], $ai_response);
// log_to_file("process_upcoming_conversations - Merged steps: ", $updated_conversation_steps);
// // Update conversation status to "generated"
$conversation_table = $wpdb->prefix . 'rl_mailwarmer_conversation';
$status = 'new';
$result = $wpdb->update(
$conversation_table,
// [],
['status' => $status, 'conversation_steps' => $updated_conversation_steps],
['id' => $conversation['id']],
['%s'],
['%s'],
['%d']
);
// log_to_file("process_upcoming_conversations - Update DB Result: ", $result);
$update_messages_db = self::update_messages_with_merged_output($conversation['id'], $updated_conversation_steps);
log_to_file("process_upcoming_conversations - Result of update_messages_with_merged_output(): ", $update_messages_db);
// Update the conversation with generated steps
// log_to_file("process_upcoming_conversations - Updating conversation_steps");
// if (RL_MailWarmer_Campaign_Helper::update_conversation_steps($conversation['id'], $updated_conversation_steps)) {
// // log_to_file("process_upcoming_conversations - Updated conversation_steps!");
// }
// Save the generated content to the conversation
// RL_MailWarmer_DB_Helper::update_conversation_steps($conversation['id'], $ai_response);
} catch (Exception $e) {
error_log("Failed to generate conversation ID {$conversation['id']}: " . $e->getMessage());
}
}
}
private static function update_messages_with_merged_output($conversation_id, $merged_output) {
log_to_file("update_messages_with_merged_output - merged_output: ", $merged_output);
global $wpdb;
$message_table = $wpdb->prefix . 'rl_mailwarmer_messages';
// $merged_output = json_decode($merged_output);
// Fetch all messages for the given conversation ID
$messages = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM $message_table WHERE conversation_id = %d ORDER BY `scheduled_for_timestamp` ASC",
$conversation_id
),
ARRAY_A
);
log_to_file("update_messages_with_merged_output - Found Messages: ", $messages);
if (empty($messages)) {
throw new Exception("No messages found for conversation ID $conversation_id.");
}
// Iterate through messages and merged output
foreach ($messages as $index => $message) {
if (!isset($merged_output[$index])) {
continue; // Skip if there's no corresponding merged output
}
$merged = $merged_output[$index];
// log_to_file("update_messages_with_merged_output - merged: $merged");
// Prepare updated data
// $updated_data = [
// 'status' => $merged['status'],
// 'scheduled_for_timestamp' => $merged['scheduled_for'],
// 'from_email' => $merged['from'],
// 'to_email' => json_encode($merged['to']),
// 'cc_email' => json_encode($merged['cc']),
// 'subject' => $merged['subject'],
// 'body' => $merged['body'],
// ];
// log_to_file("update_messages_with_merged_output - Updated Data: ", $updated_data);
// Update the database
// $wpdb->update(
// $message_table,
// $updated_data,
// ['id' => $message['id']], // Where clause
// [
// '%s', // status
// '%s', // scheduled_for_timestamp
// '%s', // from_email
// '%s', // to_email
// '%s', // cc_email
// '%s', // subject
// '%s', // body
// ],
// ['%d'] // ID
// );
}
return true;
}
/**
* Fetch a conversation from ChatGPT based on conversation parameters.
*
* @param array $conversation The conversation data.
* @return string The AI-generated conversation as JSON.
* @throws Exception If the API call fails.
*/
private static function fetch_conversation_from_ai($conversation) {
// Prepare the prompt for the AI request
$prompt = [
'profession' => $conversation['profession'],
'from' => $conversation['initiated_by'],
'to_pool' => json_decode($conversation['to_pool'], true),
'subject' => $conversation['subject'],
'num_of_replies' => $conversation['num_responses'],
'num_of_participants' => $conversation['num_participants'],
'max_days' => 3,
'can_reply' => json_decode($conversation['reply_pool'], true),
'available_to_cc' => json_decode($conversation['cc_pool'], true),
'start_date' => $conversation['first_message_timestamp'],
'end_date' => date('Y-m-d H:i:s', strtotime('+3 days', strtotime($conversation['first_message_timestamp'])))
];
// Call OpenAI API
$response = RL_MailWarmer_Api_Helper::call_openai_api($prompt);
if (!$response || !isset($response['choices'][0]['text'])) {
throw new Exception('Invalid response from OpenAI API');
}
return $response['choices'][0]['text'];
}
public static function merge_timeline_with_ai_response($conversation_json, $ai_response_json) {
// Decode the JSON inputs into arrays
$conversation = json_decode($conversation_json, true);
$ai_response = json_decode($ai_response_json, true);
// Ensure both arrays have the same number of elements
$merged = [];
$count_timeline = count($conversation);
$count_ai_response = count($ai_response);
if ($count_ai_response > $count_timeline) {
// Trim the AI responses to match the timeline count
$ai_response = array_slice($ai_response, 0, $count_timeline);
} elseif ($count_ai_response < $count_timeline) {
throw new Exception('The number of AI responses is less than the timeline steps.');
}
// Merge corresponding elements
foreach ($conversation as $index => $timeline_step) {
$ai_message = $ai_response[$index];
$merged[] = array_merge($timeline_step, $ai_message);
}
// Return the merged result as JSON
return json_encode($merged, JSON_PRETTY_PRINT);
}
}
add_action('admin_menu', function () {
add_menu_page(
__('Conversations', 'rl-mailwarmer'), // Page title
__('Conversations', 'rl-mailwarmer'), // Menu title
'manage_options', // Capability
'rl-mailwarmer-conversations', // Menu slug
'rl_mailwarmer_render_conversations_page', // Callback function
'dashicons-email', // Icon
8 // Position
);
});
/**
* Render the Conversations admin page with checkboxes and a Delete button.
*/
function rl_mailwarmer_render_conversations_page() {
if (!current_user_can('manage_options')) {
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'rl_mailwarmer_conversations'; // Conversation table
$conversations = $wpdb->get_results("SELECT * FROM $table_name ORDER BY created_at DESC");
?>
<div class="wrap">
<h1><?php esc_html_e('Conversations', 'rl-mailwarmer'); ?></h1>
<?php if (empty($conversations)) : ?>
<p><?php esc_html_e('No conversations found.', 'rl-mailwarmer'); ?></p>
<?php else : ?>
<form id="conversation-management-form" method="post">
<table class="widefat fixed striped">
<thead>
<tr>
<th style="width: 50px;">
<input type="checkbox" id="check-all-conversations">
</th>
<th><?php esc_html_e('ID', 'rl-mailwarmer'); ?></th>
<th><?php esc_html_e('Campaign ID', 'rl-mailwarmer'); ?></th>
<th><?php esc_html_e('Created At', 'rl-mailwarmer'); ?></th>
<th><?php esc_html_e('Steps', 'rl-mailwarmer'); ?></th>
<th><?php esc_html_e('Prompt', 'rl-mailwarmer'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($conversations as $conversation) : ?>
<tr>
<td>
<input type="checkbox" name="selected_conversations[]" value="<?php echo esc_attr($conversation->id); ?>">
</td>
<td><?php echo esc_html($conversation->id); ?></td>
<td><?php echo esc_html($conversation->campaign_id); ?></td>
<td><?php echo esc_html($conversation->created_at); ?></td>
<td><?php echo esc_html($conversation->conversation_steps); ?></td>
<td>
<details>
<summary><?php esc_html_e('View Prompt', 'rl-mailwarmer'); ?></summary>
<pre><?php echo esc_html($conversation->prompt); ?></pre>
</details>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p>
<button type="button" id="delete-selected-conversations" class="button button-primary">
<?php esc_html_e('Delete Items', 'rl-mailwarmer'); ?>
</button>
</p>
</form>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Select all conversations
const checkAll = document.getElementById('check-all-conversations');
const checkboxes = document.querySelectorAll('input[name="selected_conversations[]"]');
checkAll.addEventListener('change', function () {
checkboxes.forEach(checkbox => checkbox.checked = checkAll.checked);
});
// Handle delete button click
document.getElementById('delete-selected-conversations').addEventListener('click', function () {
const selectedIds = Array.from(checkboxes)
.filter(checkbox => checkbox.checked)
.map(checkbox => checkbox.value);
if (selectedIds.length === 0) {
alert('<?php esc_html_e('Please select at least one conversation to delete.', 'rl-mailwarmer'); ?>');
return;
}
if (confirm('<?php esc_html_e('Are you sure you want to delete the selected conversations?', 'rl-mailwarmer'); ?>')) {
jQuery.post(ajaxurl, {
action: 'rl_delete_conversations',
conversation_ids: selectedIds,
}, function (response) {
if (response.success) {
alert('<?php esc_html_e('Selected conversations deleted successfully.', 'rl-mailwarmer'); ?>');
location.reload(); // Reload the page to update the list
} else {
alert('<?php esc_html_e('Failed to delete conversations.', 'rl-mailwarmer'); ?>');
}
});
}
});
});
</script>
<?php
}
function rl_mailwarmer_get_conversations($page, $per_page = 20) {
global $wpdb;
$offset = ($page - 1) * $per_page;
$total = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}rl_mailwarmer_conversation");
$conversations = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}rl_mailwarmer_conversation ORDER BY created_at DESC LIMIT %d OFFSET %d",
$per_page,
$offset
));
return ['conversations' => $conversations, 'total' => $total];
}
add_action('wp_ajax_rl_delete_conversations', function () {
global $wpdb;
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => __('Permission denied.', 'rl-mailwarmer')]);
}
$conversation_ids = isset($_POST['conversation_ids']) ? array_map('intval', $_POST['conversation_ids']) : [];
if (empty($conversation_ids)) {
wp_send_json_error(['message' => __('No conversations selected.', 'rl-mailwarmer')]);
}
$table_name = $wpdb->prefix . 'rl_mailwarmer_conversation';
// Delete selected conversations
$placeholders = implode(',', array_fill(0, count($conversation_ids), '%d'));
$query = "DELETE FROM $table_name WHERE id IN ($placeholders)";
$result = $wpdb->query($wpdb->prepare($query, $conversation_ids));
if ($result === false) {
wp_send_json_error(['message' => __('Failed to delete conversations.', 'rl-mailwarmer')]);
}
wp_send_json_success(['message' => __('Conversations deleted successfully.', 'rl-mailwarmer')]);
});
// Add the metabox
add_action('add_meta_boxes', 'rl_mailwarmer_add_process_conversations_metabox');
function rl_mailwarmer_add_process_conversations_metabox() {
add_meta_box(
'rl-process-conversations',
__('Process Upcoming Conversations', 'rl-mailwarmer'),
'rl_mailwarmer_process_conversations_metabox_callback',
'campaign',
'side',
'default'
);
}
// Metabox callback function
function rl_mailwarmer_process_conversations_metabox_callback($post) {
// Nonce field for security
wp_nonce_field('rl_process_conversations_nonce', 'rl_process_conversations_nonce_field');
?>
<button type="button" id="rl_process_conversations_button" class="button button-primary">
<?php _e('Process Upcoming Conversations', 'rl-mailwarmer'); ?>
</button>
<div id="rl_process_conversations_result" style="margin-top: 10px;"></div>
<?php
}
// Enqueue JavaScript
add_action('admin_enqueue_scripts', 'rl_mailwarmer_enqueue_process_conversations_script');
function rl_mailwarmer_enqueue_process_conversations_script($hook) {
if ($hook === 'post.php' || $hook === 'post-new.php') {
wp_enqueue_script(
'rl-process-conversations',
RL_MAILWARMER_URL . 'js/rl-process-conversations.js',
['jquery'],
'1.0',
true
);
wp_localize_script('rl-process-conversations', 'rlProcessConversations', [
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('rl_process_conversations_nonce'),
]);
}
}
// AJAX handler
add_action('wp_ajax_rl_process_upcoming_conversations', 'rl_mailwarmer_process_upcoming_conversations_handler');
function rl_mailwarmer_process_upcoming_conversations_handler() {
check_ajax_referer('rl_process_conversations_nonce', 'security');
$post_id = intval($_POST['post_id']);
if (!$post_id) {
wp_send_json_error(__('Invalid campaign ID.', 'rl-mailwarmer'));
}
try {
// Call the function to process upcoming conversations
RL_MailWarmer_Conversation_Handler::process_upcoming_conversations($post_id);
wp_send_json_success(__('Conversations processed successfully.', 'rl-mailwarmer'));
} catch (Exception $e) {
wp_send_json_error(__('Error processing conversations: ', 'rl-mailwarmer') . $e->getMessage());
}
}

View file

@ -0,0 +1,627 @@
<?php
/**
* Helper functions for the email-account post type.
*/
// require 'vendor/autoload.php';
use phpseclib3\Net\SSH2;
use phpseclib3\Crypt\PublicKeyLoader;
if (!defined('ABSPATH')) {
exit;
}
/**
* RL_MailWarmer_Email_Helper Class
*
* Handles email account management and mail operations.
*/
class RL_MailWarmer_Email_Helper
{
/**
* Modify an email account post.
*
* Creates, updates, or deletes an email account post and its associated metadata.
*
* @param array $args {
* Arguments for modifying the email account.
*
* @type int $post_id Optional. Post ID to update or delete. Required for updates or deletions.
* @type string $action Required. Action to perform: 'create', 'update', or 'delete'.
* @type string $email Optional. Email address for the account (used as post title).
* @type array $metadata Optional. Additional metadata to save with the post.
* }
* @return int|bool Post ID on success, false on failure.
*/
// Create an Email Account
// $post_id = RL_MailWarmer_Email_Helper::modify_email_account([
// 'action' => 'create',
// 'email' => 'johndoe@example.com',
// 'metadata' => [
// 'full_name' => 'John Doe',
// 'mail_password' => 'securepassword123',
// 'email_provider' => 123, // Post ID of the email provider
// 'smtp_server' => 'smtp.example.com',
// 'smtp_port' => 587,
// 'imap_server' => 'imap.example.com',
// 'imap_port' => 993,
// ],
// ]);
// // Update an Email Account
// $result = RL_MailWarmer_Email_Helper::modify_email_account([
// 'action' => 'update',
// 'post_id' => $post_id,
// 'metadata' => [
// 'full_name' => 'Jane Doe',
// 'mail_password' => 'newsecurepassword123',
// 'smtp_status' => 'connected',
// ],
// ]);
// // Delete an Email Account
// $result = RL_MailWarmer_Email_Helper::modify_email_account([
// 'action' => 'delete',
// 'post_id' => $post_id,
// ]);
public static function modify_email_account(array $args)
{
// Validate required arguments
if (empty($args['action']) || !in_array($args['action'], ['create', 'update', 'delete'], true)) {
throw new InvalidArgumentException('Invalid or missing action.');
}
/*
* Add validation to only delete email-account posts
*
*/
$action = $args['action'];
$post_id = $args['post_id'] ?? null;
// Handle delete action
if ($action === 'delete') {
if (!$post_id) {
throw new InvalidArgumentException('Post ID is required for deletion.');
}
return wp_delete_post($post_id, true);
}
// Validate fields for create/update
$post_data = [
'post_type' => 'email-account',
'post_status' => 'publish',
];
// For "create", ensure no existing post with the same title
if ($action === 'create') {
if (empty($args['email'])) {
throw new InvalidArgumentException('Email is required for creating a new account.');
}
$existing_post = get_page_by_title($args['email'], OBJECT, 'email-account');
if ($existing_post) {
throw new RuntimeException('An email account with this title already exists.');
}
$post_data['post_title'] = $args['email'];
} elseif ($action === 'update') {
if (!$post_id) {
throw new InvalidArgumentException('Post ID is required for updates.');
}
$post_data['ID'] = $post_id;
}
// Assemble metadata
$meta_args = $args['metadata'] ?? [];
// Generate a random password if mail_password is not provided
if (empty($meta_args['mail_password'])) {
$meta_args['mail_password'] = bin2hex(random_bytes(8)); // 16-character password
}
$post_data['meta_input'] = array_map('sanitize_text_field', $meta_args);
// Save or update the post
$post_id = wp_insert_post($post_data);
if (is_wp_error($post_id)) {
throw new RuntimeException('Failed to save email account post: ' . $post_id->get_error_message());
}
return $post_id;
}
/**
* Check mail login credentials for an email account.
*
* Validates IMAP/SMTP connection settings for the given email account.
*
* @param mixed $email_account The email-account post object or ID.
* @param string|null $protocol Optional. The protocol to validate ('IMAP' or 'SMTP'). Defaults to both.
* @return array|WP_Error Validation results for IMAP and/or SMTP or WP_Error on failure.
*/
public static function check_mail_login($email_account, $protocol = null)
{
log_to_file("check_mail_login - Email account id: {$email_account}");
// Get the post object
$post = is_numeric($email_account) ? get_post($email_account) : $email_account;
if (!$post || $post->post_type !== 'email-account') {
return new WP_Error('invalid_post', __('Invalid email account post.', 'rl-mailwarmer'));
}
// Fetch email provider and override defaults with saved values
$email_provider_id = get_post_meta($post->ID, 'email_provider', true);
// log_to_file("check_mail_login - ");
// log_to_file("check_mail_login - Email Provider ID $email_provider_id");
$defaults = $email_provider_id ? self::get_provider_defaults($email_provider_id) : [];
// log_to_file("check_mail_login - Email Provider Defaults: ", $defaults);
// Fetch saved settings
$saved_settings = [
'full_name' => get_post_meta($post->ID, 'full_name', true),
'email_signature' => get_post_meta($post->ID, 'email_signature', true),
'mail_password' => get_post_meta($post->ID, 'mail_password', true),
'imap_password' => get_post_meta($post->ID, 'imap_password', true),
'imap_server' => get_post_meta($post->ID, 'imap_server', true),
'imap_port' => get_post_meta($post->ID, 'imap_port', true),
'smtp_password' => get_post_meta($post->ID, 'smtp_password', true),
'smtp_server' => get_post_meta($post->ID, 'smtp_server', true),
'smtp_port' => get_post_meta($post->ID, 'smtp_port', true),
];
// Merge saved settings with defaults
$settings = array_merge($defaults, array_filter($saved_settings));
// log_to_file("check_mail_login - Using settings: ", $settings);
$results = [];
// Validate IMAP connection if required
if ($protocol === null || strtoupper($protocol) === 'IMAP') {
$imap_result = self::validate_imap_connection($post->post_title, $settings);
// log_to_file("check_mail_login - IMAP Result for " . $post->post_title . ": ", $imap_result);
$results['IMAP'] = $imap_result ? __('SUCCESS', 'rl-mailwarmer') : $imap_result->get_error_message();
update_post_meta($post->ID, 'imap_status', $results['IMAP']);
}
// Validate SMTP connection if required
if ($protocol === null || strtoupper($protocol) === 'SMTP') {
$smtp_result = self::validate_smtp_connection($post->post_title, $settings);
// log_to_file("check_mail_login - SMTP Result for " . $post->post_title . ": ", $imap_result);
$results['SMTP'] = $smtp_result ? __('SUCCESS', 'rl-mailwarmer') : $smtp_result->get_error_message();
update_post_meta($post->ID, 'smtp_status', $results['SMTP']);
}
// log_to_file("check_mail_login - Full Results for " . $post->post_title . ": ", $results);
return $results;
}
/**
* Fetch default settings for an email provider.
*
* @param int $email_provider_id The post ID of the email provider.
* @return array The default server settings.
*/
public static function get_provider_defaults($email_provider_id)
{
return [
'imap_server' => get_post_meta($email_provider_id, 'default_imap_server', true),
'imap_port' => get_post_meta($email_provider_id, 'default_imap_port', true),
'smtp_server' => get_post_meta($email_provider_id, 'default_smtp_server', true),
'smtp_port' => get_post_meta($email_provider_id, 'default_smtp_port', true),
];
}
/**
* Validate an IMAP connection for an email account.
*
* @param string $email The email address.
* @param array $settings The server settings (imap_server, imap_port, imap_password).
* @return bool True if the connection is successful, false otherwise.
*/
private static function validate_imap_connection($email, $settings)
{
if ( empty($settings['imap_server']) || empty($settings['imap_port']) ) {
return false; // Missing required settings
}
if (!empty($settings['imap_password'])) {
$password = $settings['imap_password'];
} else {
$password = $settings['mail_password'];
}
// Try connecting to the IMAP server
$imap_stream = @imap_open(
'{' . $settings['imap_server'] . ':' . $settings['imap_port'] . '/imap/ssl}',
$email,
$password
);
if ($imap_stream) {
imap_close($imap_stream); // Close connection if successful
return true;
}
return false; // Connection failed
}
/**
* Validate an SMTP connection for an email account using Symfony Mailer.
*
* @param string $email The email address.
* @param array $settings The server settings (smtp_server, smtp_port, smtp_password).
* @return bool True if the connection is successful, false otherwise.
*/
private static function validate_smtp_connection($email, $settings)
{
if (empty($settings['smtp_server']) || empty($settings['smtp_port']) ) {
return false; // Missing required settings
}
if (!empty($settings['smtp_password'])) {
$password = $settings['smtp_password'];
} else {
$password = $settings['mail_password'];
}
$signature = str_replace('\n', PHP_EOL, $settings['email_signature']);
$test_to_email = "ruben@redlotusaustin.com";
$email_body = "This is a test email to verify SMTP connection for {$email}\n\n{$signature}";
try {
// Create the SMTP transport
$transport = new Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport(
$settings['smtp_server'],
$settings['smtp_port']
);
// Set authentication details
$transport->setUsername($email);
$transport->setPassword($password);
// Create the mailer
$mailer = new Symfony\Component\Mailer\Mailer($transport);
// Send a test email
$test_email = (new Symfony\Component\Mime\Email())
->from("{$settings['full_name']} <{$email}>")
->to($test_to_email)
->subject('SMTP Connection Test for ' . $email)
->html($email_body);
$mailer->send($test_email);
return true;
} catch (Exception $e) {
error_log('SMTP validation failed: ' . $e->getMessage());
return false;
}
}
/**
* Generate random email accounts for the specified domain.
*
* @param string $domain The domain name to use for the email accounts.
* @param int $qty The number of email accounts to generate. Defaults to 1.
* @return array List of generated names and email addresses.
* @throws Exception If name pools are empty or post creation fails.
*/
public static function generate_random_accounts($domain, $qty = 1)
{
$domain_post = get_post($domain);
// Fetch name pools from ACF options
$first_name_pool = get_field('valid_first_name_pool', 'option');
$last_name_pool = get_field('valid_last_name_pool', 'option');
if (empty($first_name_pool) || empty($last_name_pool)) {
throw new Exception(__('Name pools are empty. Please configure them in the ACF options.', 'rl-mailwarmer'));
}
$first_names = explode(',', $first_name_pool); // Assume comma-separated list
$last_names = explode(',', $last_name_pool);
$generated_accounts = [];
for ($i = 0; $i < $qty; $i++) {
// Generate a random name
$first_name = trim($first_names[array_rand($first_names)]);
$last_name = trim($last_names[array_rand($last_names)]);
$full_name = "{$first_name} {$last_name}";
// Generate a semi-random email address
$email_formats = [
"{$first_name}{$last_name}",
"{$first_name}.{$last_name}",
substr($first_name, 0, 1) . ".{$last_name}",
"{$first_name}.l",
substr($first_name, 0, 1) . $last_name,
];
$email_local_part = strtolower($email_formats[array_rand($email_formats)]);
$email_address = "{$email_local_part}@{$domain_post->post_title}";
// Generate a random password
$random_password = wp_generate_password(16, false, false);
$signature = self::generate_random_email_signature($first_name, $last_name, $email_address);
$servers[] = 108;
$mailferno_default_email_provider = 92;
// Create the email-account post
$post_id = wp_insert_post([
'post_type' => 'email-account',
'post_status' => 'publish',
'post_title' => $email_address,
'meta_input' => [
'full_name' => "{$first_name} {$last_name}",
'mail_password' => $random_password,
'email_signature' => $signature,
'domain' => $domain_post->ID,
'include_in_reply_pool' => true,
'include_in_cc_pool' => true,
'include_in_warmup_pool' => true,
'servers' => $servers,
'email_provider' => $mailferno_default_email_provider,
],
]);
log_to_file("generate_random_accounts - Added email account to local server: $post_id");
if ($post_id && !is_wp_error($post_id)) {
$generated_accounts[] = [
'name' => "{$first_name} {$last_name}",
'email' => $email_address,
'password' => $random_password,
'post_id' => $post_id,
];
log_to_file("generate_random_accounts - {$first_name} {$last_name}\t{$email_address}\t{$random_password}");
$add_account_result = self::modify_email_account_on_server($post_id, 'create');
log_to_file("generate_random_accounts - Result of attempting to add account to remote server: ", $add_account_result);
if ( isset($add_account_result['errors']) ) {
log_to_file("generate_random_accounts - Error modifying account on remote server: ", $add_account_result['errors']);
} else {
log_to_file("generate_random_accounts - Added $email_address to remote server: ", $add_account_result);
$login_test_results = self::check_mail_login($post_id);
log_to_file("generate_random_accounts - Login test results: ", $login_test_results);
}
} else {
error_log('Failed to create email-account post: ' . print_r($post_id, true));
}
}
return $generated_accounts;
}
/**
* Generate a randomized email signature.
*
* @param string $first_name The first name of the person.
* @param string $last_name The last name of the person.
* @param string $email_address The email address of the person.
* @return string The generated email signature.
*/
private static function generate_random_email_signature($first_name, $last_name, $email_address) {
// First line variations
$first_line_variations = [
'Best regards',
'Regards',
'Yours truly',
'Sincerely',
'Warm regards',
'Kind regards',
'Best wishes',
'Respectfully',
'With gratitude',
'All the best',
'Cheers',
'Thank you',
'Warm wishes',
'Yours sincerely',
'Cordially',
'With appreciation',
'Many thanks',
'Take care',
'Faithfully',
'Always',
];
// Randomized job titles
$job_titles = [
'Software Engineer',
'Marketing Specialist',
'Sales Manager',
'Project Coordinator',
'Product Designer',
'Customer Success Lead',
];
// Randomized phone formats
// $phone_formats = [
// '(555) %03d-%04d',
// '555-%03d-%04d',
// '+1 555 %03d %04d',
// ];
// // Social media handles (optional)
// $social_links = [
// 'LinkedIn' => 'https://linkedin.com/in/' . strtolower($first_name . $last_name),
// 'Twitter' => 'https://twitter.com/' . strtolower($first_name . $last_name),
// 'GitHub' => 'https://github.com/' . strtolower($first_name . $last_name),
// ];
// Generate random elements
$first_line = $first_line_variations[array_rand($first_line_variations)];
$job_title = $job_titles[array_rand($job_titles)];
// $phone_number = sprintf($phone_formats[array_rand($phone_formats)], rand(100, 999), rand(1000, 9999));
// $selected_social = array_rand($social_links);
// Build the email signature
$signature = "<p>{$first_line},</p>";
$signature .= "<p><strong>{$first_name} {$last_name}</strong><br>";
$signature .= "{$job_title}<br>";
$signature .= "Email: <a href='mailto:{$email_address}'>{$email_address}</a><br>";
// $signature .= "Phone: {$phone_number}<br>";
// $signature .= "<a href='{$social_links[$selected_social]}' target='_blank'>{$selected_social}</a></p>";
return $signature;
}
/**
* Modify an email account on a VirtualMin server.
*
* @param int $account_id The email-account post ID.
* @param string $action The action to perform: 'create', 'update', or 'delete'.
* @return bool|WP_Error True on success, WP_Error on failure.
*/
public static function modify_email_account_on_server($account_id, $action)
{
// Validate email-account post
$email_account = get_post($account_id);
if (!$email_account || $email_account->post_type !== 'email-account') {
return new WP_Error('invalid_account', __('Invalid email account.', 'rl-mailwarmer'));
}
// Fetch associated server posts
$domain_id = get_post_meta($account_id, 'domain', true);
if (!$domain_id) {
return new WP_Error('missing_domain', __('No associated domain found.', 'rl-mailwarmer'));
}
$server_ids = get_post_meta($domain_id, 'servers', true); // Assuming this field holds server post IDs
if (empty($server_ids) || !is_array($server_ids)) {
return new WP_Error('missing_servers', __('No associated servers found.', 'rl-mailwarmer'));
}
// Fetch email account details
$email_address = $email_account->post_title;
$password = get_post_meta($account_id, 'mail_password', true);
$full_name = get_post_meta($account_id, 'full_name', true);
[$username, $domain] = explode('@', $email_address);
// Iterate over servers and perform the action
foreach ($server_ids as $server_id) {
$server = get_post($server_id);
if (!$server || $server->post_type !== 'server') {
continue; // Skip invalid server posts
}
$server_ip = get_post_meta($server_id, 'ip_address', true);
$server_port = get_post_meta($server_id, 'ssh_port', true);
$server_user = get_post_meta($server_id, 'username', true);
$server_password = get_post_meta($server_id, 'ssh_private_key', true);
// $server_password = get_post_meta($server_id, 'password', true);
if (!$server_ip || !$server_user || !$server_password) {
return new WP_Error('missing_server_details', __('Missing server credentials.', 'rl-mailwarmer'));
}
// Build VirtualMin command
$command = "sudo virtualmin";
if ($action === 'create') {
$command .= " create-user --domain $domain --user $username --pass '$password' --real '$full_name'";
} elseif ($action === 'update') {
$command .= " modify-user --domain $domain --user $username --pass '$password' --real '$full_name'";
} elseif ($action === 'delete') {
$command .= " delete-user --domain $domain --user $username";
} else {
return new WP_Error('invalid_action', __('Invalid action specified.', 'rl-mailwarmer'));
}
log_to_file("modify_email_account_on_server - SSH Command: ", $command);
// Execute the command via SSH
// $ssh = new phpseclib\Net\SSH2($server_ip);
// $key = new phpseclib\Crypt\PublicKeyLoader::loadPrivateKey($server_password); // Adjust for SSH key or plain password
$ssh = new SSH2($server_ip, $server_port);
if (!empty($server_password)) {
// Load the private key from the postmeta field
$key = PublicKeyLoader::loadPrivateKey($server_password);
} else {
// Fallback to password-based authentication
// $key = $server_password;
log_to_file("modify_email_account_on_server - Server $$server_id ssh_private_key empty");
return new WP_Error('ssh_login_failed', __('No private key found!', 'rl-mailwarmer'));
}
if (!$ssh->login($server_user, $key)) {
return new WP_Error('ssh_login_failed', __('Failed to log into the server.', 'rl-mailwarmer'));
}
$output = $ssh->exec($command);
if (strpos($output, 'failed') !== false) {
return new WP_Error('command_failed', __('Failed to execute VirtualMin command.', 'rl-mailwarmer'));
}
}
return true; // Success
}
}
add_action('restrict_manage_posts', function ($post_type) {
if ($post_type === 'email-account') {
?>
<button id="test-connections-button" class="button button-primary" style="margin-left: 10px;">
<?php esc_html_e('Test Selected Connections', 'rl-mailwarmer'); ?>
</button>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.getElementById('test-connections-button').addEventListener('click', function (event) {
event.preventDefault(); // Prevent form submission
const selectedAccounts = Array.from(document.querySelectorAll('.check-column input[type="checkbox"]:checked'))
.map(checkbox => checkbox.value)
.filter(value => value !== 'on'); // Exclude the "select all" checkbox
if (selectedAccounts.length === 0) {
alert('<?php esc_html_e('Please select at least one account.', 'rl-mailwarmer'); ?>');
return;
}
if (confirm('<?php esc_html_e('Are you sure you want to test connections for the selected accounts?', 'rl-mailwarmer'); ?>')) {
jQuery.post(ajaxurl, {
action: 'rl_test_connections',
account_ids: selectedAccounts
}, function (response) {
alert(response.data.message);
});
}
});
});
</script>
<?php
}
});
add_action('wp_ajax_rl_test_connections', function () {
$account_ids = isset($_POST['account_ids']) ? array_map('intval', $_POST['account_ids']) : [];
if (empty($account_ids)) {
wp_send_json_error(['message' => __('No email accounts selected.', 'rl-mailwarmer')]);
}
foreach ($account_ids as $account_id) {
// Call the check_mail_login function for this email account
$result = RL_MailWarmer_Email_Helper::check_mail_login($account_id);
}
wp_send_json_success(['message' => __('Connections tested successfully for selected accounts.', 'rl-mailwarmer')]);
});

View file

@ -0,0 +1,513 @@
<?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()]);
}
});

View file

@ -0,0 +1,155 @@
<?php
add_action('admin_menu', function () {
add_menu_page(
__('Messages', 'rl-mailwarmer'), // Page title
__('Messages', 'rl-mailwarmer'), // Menu title
'manage_options', // Capability
'rl-mailwarmer-messages', // Menu slug
'rl_mailwarmer_render_messages_page', // Callback function
'dashicons-email-alt', // Icon
9 // Position
);
});
/**
* Render the Messages admin page with checkboxes and a Delete button.
*/
function rl_mailwarmer_render_messages_page() {
if (!current_user_can('manage_options')) {
return;
}
global $wpdb;
$conversation_table = $wpdb->prefix . 'rl_mailwarmer_conversations';
$message_table = $wpdb->prefix . 'rl_mailwarmer_messages';
// Fetch conversations with their messages
$conversations = $wpdb->get_results("
SELECT c.id as conversation_id, c.campaign_id, c.created_at, m.id as message_id, m.scheduled_for_timestamp, m.from_email, m.to_email, m.subject
FROM $conversation_table c
LEFT JOIN $message_table m ON c.id = m.conversation_id
WHERE m.status != 'scheduled'
ORDER BY c.created_at DESC, m.scheduled_for_timestamp ASC
");
?>
<div class="wrap">
<h1><?php esc_html_e('Messages', 'rl-mailwarmer'); ?></h1>
<?php if (empty($conversations)) : ?>
<p><?php esc_html_e('No messages found.', 'rl-mailwarmer'); ?></p>
<?php else : ?>
<form id="message-management-form" method="post">
<table class="widefat fixed striped">
<thead>
<tr>
<th style="width: 50px;">
<input type="checkbox" id="check-all-messages">
</th>
<th><?php esc_html_e('Conversation ID', 'rl-mailwarmer'); ?></th>
<th><?php esc_html_e('Campaign ID', 'rl-mailwarmer'); ?></th>
<th><?php esc_html_e('Created At', 'rl-mailwarmer'); ?></th>
<th><?php esc_html_e('Message ID', 'rl-mailwarmer'); ?></th>
<th><?php esc_html_e('Scheduled For', 'rl-mailwarmer'); ?></th>
<th><?php esc_html_e('From', 'rl-mailwarmer'); ?></th>
<th><?php esc_html_e('To', 'rl-mailwarmer'); ?></th>
<th><?php esc_html_e('Subject', 'rl-mailwarmer'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($conversations as $conversation) : ?>
<tr>
<td>
<?php if ($conversation->message_id) : ?>
<input type="checkbox" name="selected_messages[]" value="<?php echo esc_attr($conversation->message_id); ?>">
<?php endif; ?>
</td>
<td><?php echo esc_html($conversation->conversation_id); ?></td>
<td><?php echo esc_html($conversation->campaign_id); ?></td>
<td><?php echo esc_html($conversation->created_at); ?></td>
<td><?php echo esc_html($conversation->message_id); ?></td>
<td><?php echo esc_html($conversation->scheduled_for_timestamp); ?></td>
<td><?php echo esc_html($conversation->from_email); ?></td>
<td><?php echo esc_html($conversation->to_email); ?></td>
<td><?php echo esc_html($conversation->subject); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p>
<button type="button" id="delete-selected-messages" class="button button-primary">
<?php esc_html_e('Delete Items', 'rl-mailwarmer'); ?>
</button>
</p>
</form>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Select all messages
const checkAll = document.getElementById('check-all-messages');
const checkboxes = document.querySelectorAll('input[name="selected_messages[]"]');
checkAll.addEventListener('change', function () {
checkboxes.forEach(checkbox => checkbox.checked = checkAll.checked);
});
// Handle delete button click
document.getElementById('delete-selected-messages').addEventListener('click', function () {
const selectedIds = Array.from(checkboxes)
.filter(checkbox => checkbox.checked)
.map(checkbox => checkbox.value);
if (selectedIds.length === 0) {
alert('<?php esc_html_e('Please select at least one message to delete.', 'rl-mailwarmer'); ?>');
return;
}
if (confirm('<?php esc_html_e('Are you sure you want to delete the selected messages?', 'rl-mailwarmer'); ?>')) {
jQuery.post(ajaxurl, {
action: 'rl_delete_messages',
message_ids: selectedIds,
}, function (response) {
if (response.success) {
alert('<?php esc_html_e('Selected messages deleted successfully.', 'rl-mailwarmer'); ?>');
location.reload(); // Reload the page to update the list
} else {
alert('<?php esc_html_e('Failed to delete messages.', 'rl-mailwarmer'); ?>');
}
});
}
});
});
</script>
<?php
}
add_action('wp_ajax_rl_delete_messages', function () {
global $wpdb;
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => __('Permission denied.', 'rl-mailwarmer')]);
}
$message_ids = isset($_POST['message_ids']) ? array_map('intval', $_POST['message_ids']) : [];
if (empty($message_ids)) {
wp_send_json_error(['message' => __('No messages selected.', 'rl-mailwarmer')]);
}
$table_name = $wpdb->prefix . 'rl_mailwarmer_messages';
// Delete selected messages
$placeholders = implode(',', array_fill(0, count($message_ids), '%d'));
$query = "DELETE FROM $table_name WHERE id IN ($placeholders)";
$result = $wpdb->query($wpdb->prepare($query, $message_ids));
if ($result === false) {
wp_send_json_error(['message' => __('Failed to delete messages.', 'rl-mailwarmer')]);
}
wp_send_json_success(['message' => __('Messages deleted successfully.', 'rl-mailwarmer')]);
});

View file

@ -0,0 +1,143 @@
<?php
class PostTypeList {
private string $post_type;
private array $labels;
private int $items_per_page;
private int $paged;
private array $additional_query_args;
public function __construct(string $post_type, array $labels = [], array $additional_query_args = []) {
$this->post_type = $post_type;
$this->items_per_page = isset($_GET['per_page']) ? intval($_GET['per_page']) : 10;
$this->paged = (get_query_var('paged')) ? get_query_var('paged') : 1;
// Base query args
$base_args = [
'orderby' => 'title',
'order' => 'ASC',
'post_type' => $this->post_type,
'posts_per_page' => $this->items_per_page,
'paged' => $this->paged,
];
// Add owner_id filter only for non-administrators
if (!current_user_can('administrator')) {
$base_args['meta_query'] = array(
array(
'key' => 'owner_id',
'value' => get_current_user_id(),
'compare' => '='
)
);
}
// Merge with any additional query args
$this->additional_query_args = array_merge($base_args, $additional_query_args);
// Default labels that can be overridden
$this->labels = array_merge([
'title' => ucfirst($post_type) . 's',
'no_items' => 'No ' . strtolower($post_type) . 's found.',
'delete_confirm' => 'Are you sure you want to delete this ' . strtolower($post_type) . '?'
], $labels);
}
public function render(): void {
$query = new WP_Query($this->additional_query_args);
$edit_url_base = "/dashboard/" . $this->post_type . "s/edit-" . $this->post_type . "?edit=";
?>
<div class="wrap">
<!-- <h1><?php //echo esc_html($this->labels['title']); ?></h1> -->
<!-- Items list -->
<div class="post-type-list">
<?php if ($query->have_posts()) : ?>
<?php
while ($query->have_posts()) : $query->the_post();
$post_id = get_the_ID();
?>
<div class="post-type-item">
<div class="post-type-content">
<a href="<?php //echo get_permalink(); ?>" class="post-type-name">
<?php the_title(); ?>
</a>
<div class="row-actions">
<span class="edit">
<a href="<?php //echo $edit_url_base . $post_id; ?>">Edit</a> |
</span>
<span class="trash">
<?php //echo $this->get_delete_link($post_id); ?>
</span>
</div>
</div>
</div>
<?php endwhile; ?>
<?php else : ?>
<p><?php echo esc_html($this->labels['no_items']); ?></p>
<?php endif; ?>
</div>
<div class="page-nav">
<div class="tablenav-pages">
<?php
echo paginate_links(array(
'base' => add_query_arg('paged', '%#%'),
'format' => '',
'prev_text' => __('&laquo;'),
'next_text' => __('&raquo;'),
'total' => $query->max_num_pages,
'current' => $this->paged
));
?>
</div>
</div>
<?php wp_reset_postdata(); ?>
</div>
<script>
function handlePerPageChange(select) {
// Remove the paged parameter if it exists in the URL
var form = document.getElementById('items-per-page-form');
var currentPath = window.location.pathname;
// Remove any existing page/X/ from the path
currentPath = currentPath.replace(/page\/\d+\/?/, '');
// Update form action to use the cleaned path
form.action = currentPath;
// console.log(currentPath);
// Submit the form
form.submit();
}
</script>
<?php
}
protected function get_delete_link($post_id) {
if (!can_delete_post($post_id)) {
return '';
}
$current_url = add_query_arg(array()); // Gets current URL with existing parameters
$delete_url = wp_nonce_url(
add_query_arg(
array(
'action' => 'delete',
'post' => $post_id,
'redirect_to' => urlencode($current_url)
),
$current_url // Use current page URL instead of admin URL
),
'delete-post_' . $post_id
);
return sprintf(
'<a href="%s" onclick="return confirm(\'Are you sure you want to delete this item?\');" class="delete">Delete</a>',
esc_url($delete_url)
);
}
}

View file

@ -0,0 +1,185 @@
<?php
add_action('admin_menu', function () {
add_menu_page(
__('Import Data', 'rl-mailwarmer'), // Page title
__('Import Data', 'rl-mailwarmer'), // Menu title
'manage_options', // Capability
'rl-mailwarmer-import-data', // Menu slug
'rl_mailwarmer_render_import_page', // Callback function
'dashicons-upload', // Icon
25 // Position
);
});
/**
* Render the Import Data admin page.
*/
function rl_mailwarmer_render_import_page() {
if (!current_user_can('manage_options')) {
return;
}
// Check for a nonce and handle file upload
if (isset($_POST['rl_import_nonce']) && wp_verify_nonce($_POST['rl_import_nonce'], 'rl_import_data')) {
if (!empty($_FILES['domain_csv']['tmp_name'])) {
rl_mailwarmer_process_domain_csv($_FILES['domain_csv']['tmp_name']);
}
if (!empty($_FILES['email_account_csv']['tmp_name'])) {
rl_mailwarmer_process_email_account_csv($_FILES['email_account_csv']['tmp_name']);
}
}
?>
<div class="wrap">
<h1><?php esc_html_e('Import Data', 'rl-mailwarmer'); ?></h1>
<form method="post" enctype="multipart/form-data">
<?php wp_nonce_field('rl_import_data', 'rl_import_nonce'); ?>
<h2><?php esc_html_e('Import Domains', 'rl-mailwarmer'); ?></h2>
<p><?php esc_html_e('Upload a CSV file to import domains.', 'rl-mailwarmer'); ?></p>
<input type="file" name="domain_csv" accept=".csv" />
<h2><?php esc_html_e('Import Email Accounts', 'rl-mailwarmer'); ?></h2>
<p><?php esc_html_e('Upload a CSV file to import email accounts.', 'rl-mailwarmer'); ?></p>
<input type="file" name="email_account_csv" accept=".csv" />
<p><button type="submit" class="button button-primary"><?php esc_html_e('Upload Files', 'rl-mailwarmer'); ?></button></p>
</form>
</div>
<?php
}
function rl_mailwarmer_process_domain_csv($file_path) {
$handle = fopen($file_path, 'r');
if (!$handle) {
echo '<div class="error"><p>' . __('Failed to open the domain CSV file.', 'rl-mailwarmer') . '</p></div>';
return;
}
// Read the headers
$headers = fgetcsv($handle);
while (($row = fgetcsv($handle)) !== false) {
$data = array_combine($headers, $row);
if (empty($data['name'])) {
continue; // Skip invalid rows
}
// Create or update the domain post
$existing_domain = get_posts([
'post_type' => 'domain',
'title' => $data['name'],
'posts_per_page' => 1,
'fields' => 'ids',
]);
$post_id = $existing_domain[0] ?? null;
$post_data = [
'post_title' => $data['name'],
'post_type' => 'domain',
'post_status' => 'publish',
];
if ($post_id) {
$post_data['ID'] = $post_id;
}
$post_id = wp_insert_post($post_data);
if (!is_wp_error($post_id)) {
update_post_meta($post_id, 'cloudflare_api_email', sanitize_text_field($data['cloudflare_api_email']));
update_post_meta($post_id, 'cloudflare_api_key', sanitize_text_field($data['cloudflare_api_key']));
log_to_file("rl_mailwarmer_process_domain_csv - Imported domain: " . $data['name']);
} else {
log_to_file("rl_mailwarmer_process_domain_csv - ERROR importing domain: " . $data['name']);
}
}
fclose($handle);
echo '<div class="updated"><p>' . __('Domains imported successfully.', 'rl-mailwarmer') . '</p></div>';
}
function rl_mailwarmer_process_email_account_csv($file_path) {
$handle = fopen($file_path, 'r');
if (!$handle) {
echo '<div class="error"><p>' . __('Failed to open the email account CSV file.', 'rl-mailwarmer') . '</p></div>';
return;
}
// Read the headers
$headers = fgetcsv($handle);
while (($row = fgetcsv($handle)) !== false) {
$data = array_combine($headers, $row);
if (empty($data['Email Address']) || empty($data['Password'])) {
continue; // Skip invalid rows
}
if (!empty($data['Name'])) {
$name = sanitize_text_field($data['Name']);
}
if (!empty($data['Signature'])) {
$signature = sanitize_textarea_field(wp_slash($data['Signature']));
// log_to_file("rl_mailwarmer_process_email_account_csv - Signature: {$signature}");
}
// Match the email account to a domain
$domain_name = strtolower(substr(strrchr($data['Email Address'], "@"), 1));
$domain = get_posts([
'post_type' => 'domain',
'title' => $domain_name,
'post_status' => 'publish',
'posts_per_page' => 1,
'fields' => 'ids',
]);
// Match the email provider by name
$email_provider = $data['Email Provider'];
// Check if this is a scrubber account
if (!empty($data['Is Scrubber'])) {
$include_in_scrubber_pool = true;
} else {
$include_in_scrubber_pool = false;
}
// Create or update the email account post
$post_data = [
'post_title' => strtolower($data['Email Address']),
'post_type' => 'email-account',
'post_status' => 'publish',
'meta_input' => [
'full_name' => $name,
'mail_password' => sanitize_text_field($data['Password']),
'email_provider' => $email_provider,
'email_signature' => $signature,
'include_in_scrubber_pool' => $include_in_scrubber_pool,
],
];
if (!empty($domain)) {
$post_data['meta_input']['domain'] = $domain[0];
}
$post_id = wp_insert_post($post_data);
if (!is_wp_error($post_id)) {
// $email_account_connection_status = RL_MailWarmer_Email_Helper::check_mail_login($post_id);
log_to_file("rl_mailwarmer_process_email_account_csv - Imported email {$data['Email Address']}: ");
} else {
log_to_file("rl_mailwarmer_process_email_account_csv - ERROR importing email: " . $data['Email Address']);
}
}
fclose($handle);
echo '<div class="updated"><p>' . __('Email accounts imported successfully.', 'rl-mailwarmer') . '</p></div>';
}

38
js/admin-update-mx.js Normal file
View file

@ -0,0 +1,38 @@
jQuery(document).ready(function ($) {
$('#update-mx-record-button').on('click', function (e) {
e.preventDefault();
// const postId = $('#post_ID').val();
const postId = rlMailWarmerMx.post_id;
const action = $('#mx_action').val();
const content = $('#mx_content').val();
const priority = $('#mx_priority').val();
const ttl = $('#mx_ttl').val();
$('#mx-update-result').html('<p>Updating MX record...</p>');
$.ajax({
url: rlMailWarmerMx.ajax_url,
method: 'POST',
data: {
action: 'rl_mailwarmer_update_mx_record',
post_id: postId,
action_type: action,
content: content,
priority: priority,
ttl: ttl,
security: rlMailWarmerMx.nonce,
},
success: function (response) {
if (response.success) {
$('#mx-update-result').html('<p>' + response.data + '</p>');
} else {
$('#mx-update-result').html('<p>Error: ' + response.data + '</p>');
}
},
error: function (xhr, status, error) {
$('#mx-update-result').html('<p>AJAX Error: ' + error + '</p>');
},
});
});
});

View file

@ -0,0 +1,36 @@
jQuery(document).ready(function ($) {
$('#generate-conversation').on('click', function () {
const initiatedBy = $('#initiated_by').val();
const subject = $('#subject').val();
const length = $('#length').val();
$.ajax({
url: rlGenerateConversation.ajax_url,
type: 'POST',
data: {
action: 'rl_generate_conversation',
nonce: rlGenerateConversation.nonce,
post_id: rlGenerateConversation.post_id,
initiated_by: initiatedBy,
subject: subject,
length: length,
},
beforeSend: function () {
$('#generate-conversation').text('Generating...').prop('disabled', true);
},
success: function (response) {
$('#generate-conversation').text('Generate Conversation').prop('disabled', false);
if (response.success) {
console.log('Conversation generated successfully! ID: ' + response.data.conversation_id);
} else {
alert('Error: ' + response.data.message);
}
},
error: function () {
$('#generate-conversation').text('Generate Conversation').prop('disabled', false);
alert('Failed to generate conversation. Please try again.');
},
});
});
});

View file

@ -0,0 +1,35 @@
jQuery(document).ready(function ($) {
// console.log("Loaded check-domain-health.js");
$('#check-domain-health-button').on('click', function (e) {
e.preventDefault();
//var postId = $('#post_ID').val(); // Get the current post ID
var postData = {
action: 'rl_mailwarmer_check_domain_health',
post_id: rlMailWarmer_public.post_id,
security: rlMailWarmer_public.nonce,
};
// console.log("AJAX URL: " + rlMailWarmer_public.ajax_url);
// console.log("Post Action: " + postData.action);
// console.log("Post postId: " + postData.post_id);
// console.log("Post security: " + postData.security);
$('#domain-health-result').html('<p>Checking domain health...</p>');
$.ajax({
url: rlMailWarmer_public.ajax_url,
type: 'POST',
data: postData,
success: function (response) {
if (response.success) {
$('#domain-health-result').html('<p>Report saved successfully. Post ID: ' + response.data + '</p>');
} else {
$('#domain-health-result').html('<p>Error: ' + response.data + '</p>');
}
},
error: function (xhr, status, error) {
$('#domain-health-result').html('<p>AJAX Error: ' + error + '</p>');
},
});
});
});

View file

@ -0,0 +1,35 @@
jQuery(document).ready(function ($) {
$('#rl_generate_accounts_button').on('click', function () {
const postId = $('#post_ID').val();
const quantity = $('#rl_generate_account_qty').val();
$('#rl_generate_accounts_result').html('<p>Generating accounts...</p>');
$.ajax({
url: rlGenerateAccounts.ajax_url,
method: 'POST',
data: {
action: 'rl_generate_random_accounts',
post_id: postId,
qty: quantity,
security: rlGenerateAccounts.nonce,
},
success: function (response) {
if (response.success) {
const results = response.data;
let resultHtml = '<p>Generated Accounts:</p><ul>';
results.forEach(account => {
resultHtml += `<li>${account.email} (Password: ${account.password})</li>`;
});
resultHtml += '</ul>';
$('#rl_generate_accounts_result').html(resultHtml);
} else {
$('#rl_generate_accounts_result').html(`<p>Error: ${response.data}</p>`);
}
},
error: function () {
$('#rl_generate_accounts_result').html('<p>Failed to generate accounts.</p>');
},
});
});
});

View file

@ -0,0 +1,27 @@
jQuery(document).ready(function ($) {
$('#rl_process_conversations_button').on('click', function () {
const postId = $('#post_ID').val();
$('#rl_process_conversations_result').html('<p>Processing conversations...</p>');
$.ajax({
url: rlProcessConversations.ajax_url,
method: 'POST',
data: {
action: 'rl_process_upcoming_conversations',
post_id: postId,
security: rlProcessConversations.nonce,
},
success: function (response) {
if (response.success) {
$('#rl_process_conversations_result').html(`<p>${response.data}</p>`);
} else {
$('#rl_process_conversations_result').html(`<p>Error: ${response.data}</p>`);
}
},
error: function () {
$('#rl_process_conversations_result').html('<p>Failed to process conversations.</p>');
},
});
});
});

14
templates/domain-list.php Normal file
View file

@ -0,0 +1,14 @@
<?php
// log_to_file("Running domain template");
// get_header();
// $list = new PostTypeList('domain', [
// 'title' => 'My Domains',
// 'no_items' => 'You haven\'t added any domains yet.',
// 'delete_confirm' => 'Are you sure you want to delete this domain? This action cannot be undone.'
// ]);
// $list->render();
// get_footer();