diff --git a/includes/class-rl-mailwarmer-campaign-helper.php b/includes/class-rl-mailwarmer-campaign-helper.php
index fe037b0..1510417 100644
--- a/includes/class-rl-mailwarmer-campaign-helper.php
+++ b/includes/class-rl-mailwarmer-campaign-helper.php
@@ -5,8 +5,55 @@
*/
class RL_MailWarmer_Campaign_Helper
{
+
+
/**
- * Calculate the campaign timeline with randomized daily email goals.
+ * Generate and save a unique 16-digit campaign tracking ID.
+ *
+ * @param int $campaign_id The ID of the campaign.
+ * @return void
+ * @throws Exception If unable to generate a unique ID.
+ */
+ public static function generate_campaign_tracking_id($campaign_id)
+ {
+ $max_attempts = 10;
+ $attempts = 0;
+
+ while ($attempts < $max_attempts) {
+ $tracking_id = str_pad(mt_rand(0, 9999999999), 10, '0', STR_PAD_LEFT);
+
+ // Check if the ID is unique (not used by any campaign)
+ $query = new WP_Query([
+ 'post_type' => 'campaign',
+ 'meta_query' => [
+ [
+ 'key' => 'campaign_tracking_id',
+ 'value' => $tracking_id,
+ 'compare' => '=',
+ ],
+ ],
+ 'posts_per_page' => 1,
+ 'fields' => 'ids'
+ ]);
+
+ if (!$query->have_posts()) {
+ // The ID is unique, save it
+ log_to_file("generate_campaign_tracking_id - Campaign: {$campaign_id} ID: {$tracking_id}");
+ update_post_meta($campaign_id, 'campaign_tracking_id', $tracking_id);
+ return true;
+ }
+ $attempts++;
+ }
+
+ // If maximum attempts exceeded, throw an exception
+ log_to_file("generate_campaign_tracking_id - Unable to generate a unique campaign tracking ID!");
+ throw new Exception(__('Unable to generate a unique campaign tracking ID.', 'rl-mailwarmer'));
+
+ }
+
+
+ /**
+ * Calculate the campaign timeline with semi-randomized daily email goals.
*
* @param int $campaign_id The campaign post ID.
* @return array Weekly email schedule with randomized daily goals.
@@ -14,11 +61,19 @@ class RL_MailWarmer_Campaign_Helper
*/
public static function calculate_campaign_timeline($campaign_id)
{
+
+ action_log("calculate_campaign_timeline - Creating timeline for campaign {$campaign_id}");
+
// Fetch campaign details
$warmup_period = (int) get_post_meta($campaign_id, 'warmup_period', true); // Weeks
$target_volume = (int) get_post_meta($campaign_id, 'target_volume', true); // Daily emails to ramp up to
+ $weekend_reduction_factor = get_field('weekend_reduction_factor', $campaign_id) ? (int) get_field('weekend_reduction_factor', $campaign_id) : 25;
+ // Make it so the volume is REDUCED by this percentage, not capped at it
+ $weekend_reduction_factor = 100 - $weekend_reduction_factor;
$start_date = get_post_meta($campaign_id, 'start_date', true); // Campaign start date
+ // log_to_file("calculate_campaign_timeline - Target weekend reduction factor: {$weekend_reduction_factor}");
+
if (!$warmup_period || !$target_volume || !$start_date) {
throw new Exception(__('Invalid campaign parameters.', 'rl-mailwarmer'));
}
@@ -44,9 +99,9 @@ class RL_MailWarmer_Campaign_Helper
// Calculate daily ramp-up rate
- $daily_increase = ($target_volume - $starting_daily_volume) / ($total_days * .75);
+ $base_daily_increase = ($target_volume - $starting_daily_volume) / ($total_days * 0.75);
- // log_to_file("calculate_campaign_timeline - Ramping up from $min_starting_volume to $target_volume over $total_days days, increasing by $daily_increase each day with no more than $max_daily_volume emails in a day");
+ log_to_file("calculate_campaign_timeline - Ramping up from $min_starting_volume to $target_volume over $total_days days, increasing by $base_daily_increase (+/- 25%) each day with no more than $max_daily_volume emails in a day");
// Generate timeline
for ($day = 0; $day < $total_days; $day++) {
@@ -57,118 +112,153 @@ class RL_MailWarmer_Campaign_Helper
// Adjust for holidays and weekends
$is_weekend = in_array($current_date->format('N'), [6, 7]);
$is_holiday = in_array($date_formatted, $holidays);
- $reduction_factor = ($is_weekend || $is_holiday) ? mt_rand(65, 82) / 100 : 1;
+ $reduction_factor = ($is_weekend || $is_holiday) ? mt_rand($weekend_reduction_factor - 5, $weekend_reduction_factor + 5) / 100 : 1;
+
+ // log_to_file("calculate_campaign_timeline - Using daily reduction factor: {$reduction_factor}");
// Calculate daily volume
+
+ $percentage_variation = mt_rand(-25, 25);
+ $daily_increase = round($base_daily_increase + ($base_daily_increase * $percentage_variation / 100), 2);
$daily_volume = min(
ceil(($starting_daily_volume + ($daily_increase * $day)) * $reduction_factor),
- ceil($target_volume + ($target_volume * mt_rand(5,20))/100)
+ ceil($target_volume + ($target_volume * mt_rand(5, 20)) / 100)
);
-
-
- /*$daily_volume = ceil(($starting_daily_volume + ($daily_increase * $day)) * $reduction_factor);
- if ($daily_volume >= $target_volume) {
- $daily_volume = ceil( $target_volume + ($target_volume * mt_rand(5,20))/100 );
- }*/
+ log_to_file("calculate_campaign_timeline - Daily increase {$daily_increase}");
if ($daily_volume > $max_daily_volume) {
- log_to_file("calculate_campaign_timeline - Max Daily Volume hit for campaign $campaign_id! Capping number of emails");
+ // log_to_file("calculate_campaign_timeline - Max Daily Volume hit for campaign $campaign_id! Capping number of emails");
$daily_volume = $max_daily_volume;
}
- $timeline[$date_formatted] = [ 'target_volume' => $daily_volume, 'current_volume' => 0, 'items_sent' => 0 ];
+ // Calculate percent daily change
+ $percent_daily_change = $day > 0 ? (($daily_volume - $last_daily_volume) / $last_daily_volume) * 100 : 0;
+
+ // Round to 2 decimal places
+ $percent_daily_change = round($percent_daily_change, 2);
+
+ // Add a "+" sign if positive
+ $formatted_percent_change = $percent_daily_change > 0 ? '+' . $percent_daily_change : (string)$percent_daily_change;
+
+ $last_daily_volume = $daily_volume;
+
+ $timeline[$date_formatted] = [
+ 'target_volume' => $daily_volume,
+ 'current_volume' => 0,
+ 'items_sent' => 0,
+ 'percent_daily_change' => $formatted_percent_change,
+ 'daily_increase' => $daily_increase,
+ ];
+
// log_to_file("calculate_campaign_timeline - $day: $daily_volume");
- // array_push($timeline, )
- // $timeline[$date_formatted] = $daily_volume;
}
- // Save the timeline as a JSON string to the campaign post
- // $timeline_json = json_encode($timeline);
- // update_post_meta($campaign_id, 'campaign_timeline', $timeline_json);
-
- // log_to_file("calculate_campaign_timeline - Empty Timeline: $timeline_json");
- $filled_timeline = self::fill_campaign_timeline($campaign_id, $timeline);
- log_to_file("calculate_campaign_timeline - Filled Timeline: ", $filled_timeline);
-
- // Check the number of saved messages per date
- $message_counts = RL_MailWarmer_DB_Helper::get_message_counts_by_date($campaign_id);
- // log_to_file("fill_campaign_timeline - Message counts: ", $message_counts);
-
// Save the updated campaign timeline
- update_post_meta($campaign_id, 'campaign_timeline', json_encode($filled_timeline, JSON_PRETTY_PRINT));
- update_post_meta($campaign_id, 'message_counts', json_encode($message_counts, JSON_PRETTY_PRINT));
+ update_post_meta($campaign_id, 'campaign_timeline', json_encode($timeline, JSON_PRETTY_PRINT));
+ update_post_meta($campaign_id, 'campaign_timeline_json', $timeline);
+
+ if( self::fill_campaign_timeline($campaign_id, $timeline) )
+ {
+ // Check the number of saved messages per date
+ $message_counts = RL_MailWarmer_DB_Helper::get_message_counts_by_date($campaign_id);
+ update_post_meta($campaign_id, 'message_counts', json_encode($message_counts, JSON_PRETTY_PRINT));
+ action_log("calculate_campaign_timeline - Finished creating timeline for campaign {$campaign_id}");
-
- return $timeline;
+ return true;
+ } else {
+ log_to_file("calculate_campaign_timeline - Error filling campaign timeline!");
+ throw new Exception(__("Error filling campaign timeline for campaign ID: {$campaign_id}", 'rl-mailwarmer'));
+ }
}
-
- public static function fill_campaign_timeline(int $campaign_id, array $campaign_timeline): array {
+ /**
+ * Generate conversation placeholders of varying lengths and allocate them until all dates in the campaign meet the targets set by RL_MailWarmer_Campaign_Helper::calculate_campaign_timeline()
+ *
+ * @param int $campaign_id The campaign post ID.
+ * @return array Weekly email schedule with randomized daily goals.
+ * @throws Exception If the campaign parameters are invalid.
+ */
+ public static function fill_campaign_timeline(int $campaign_id, $campaign_timeline = null) {
$filled_dates = [];
$weekly_volumes = [];
$current_week = '';
$weekly_total = 0;
- $ratios = [
- 'extra-long' => [
- 'percent_of_volume_lower' => 0,
- 'percent_of_volume_upper' => 0,
- 'num_participants_lower' => 2,
- 'num_participants_upper' => 8,
- 'num_responses_lower' => 5,
- 'num_responses_upper' => 12
- ],
- 'long' => [
- 'percent_of_volume_lower' => 2,
- 'percent_of_volume_upper' => 4,
- 'num_participants_lower' => 3,
- 'num_participants_upper' => 6,
- 'num_responses_lower' => 4,
- 'num_responses_upper' => 6
- ],
- 'medium' => [
- 'percent_of_volume_lower' => 2,
- 'percent_of_volume_upper' => 6,
- 'num_participants_lower' => 3,
- 'num_participants_upper' => 5,
- 'num_responses_lower' => 3,
- 'num_responses_upper' => 5
- ],
- 'short' => [
- 'percent_of_volume_lower' => 15,
- 'percent_of_volume_upper' => 20,
- 'num_participants_lower' => 2,
- 'num_participants_upper' => 4,
- 'num_responses_lower' => 2,
- 'num_responses_upper' => 4
- ],
- ];
+ $total_conversation_count = 0;
+ $total_message_count = 0;
+ $campaign_limited = get_post_meta($campaign_id, 'campaign_limited', true);
+ if (!$campaign_timeline) {
+ try {
+ $campaign_timeline = get_field('campaign_timeline', $campaign_id);
+
+ } catch (Exception $e) {
+
+ log_to_file("fill_campaign_timeline - No campaign timeline found for campaign ID: {$campaign_id}");
+ throw new Exception(__("No campaign timeline found for campaign ID: {$campaign_id}", 'rl-mailwarmer'));
+ }
+ }
+ if (!$campaign_limited ) {
+ $ratios = [
+ 'extra-long' => [
+ 'percent_of_volume_lower' => 1,
+ 'percent_of_volume_upper' => 3,
+ 'num_participants_lower' => 1,
+ 'num_participants_upper' => 8,
+ 'num_responses_lower' => 5,
+ 'num_responses_upper' => 10
+ ],
+ 'long' => [
+ 'percent_of_volume_lower' => 2,
+ 'percent_of_volume_upper' => 4,
+ 'num_participants_lower' => 1,
+ 'num_participants_upper' => 6,
+ 'num_responses_lower' => 4,
+ 'num_responses_upper' => 6
+ ],
+ 'medium' => [
+ 'percent_of_volume_lower' => 2,
+ 'percent_of_volume_upper' => 6,
+ 'num_participants_lower' => 1,
+ 'num_participants_upper' => 5,
+ 'num_responses_lower' => 3,
+ 'num_responses_upper' => 5
+ ],
+ 'short' => [
+ 'percent_of_volume_lower' => 15,
+ 'percent_of_volume_upper' => 20,
+ 'num_participants_lower' => 1,
+ 'num_participants_upper' => 4,
+ 'num_responses_lower' => 2,
+ 'num_responses_upper' => 4
+ ],
+ ];
+ } else {
+ log_to_file("fill_campaign_timeline - Creating a limited campaign");
+ $ratios = [
+ 'short' => [
+ 'percent_of_volume_lower' => 100,
+ 'percent_of_volume_upper' => 100,
+ 'num_participants_lower' => 1,
+ 'num_participants_upper' => 5,
+ 'num_responses_lower' => 1,
+ 'num_responses_upper' => 1
+ ],
+ ];
+ }
$warmup_period = (int) get_post_meta($campaign_id, 'warmup_period', true); // Weeks
$start_date = get_post_meta($campaign_id, 'start_date', true); // Campaign start date
$total_days = $warmup_period * 7; // Total days in the campaign
+ // $total_days = $warmup_period; // Total days in the campaign
$start_date = date('Y-m-d H:i:s', strtotime($start_date));
$end_date = date('Y-m-d 23:59:59', strtotime($start_date . " + {$total_days} days"));
// $end_date = date('Y-m-d H:i', strtotime($start_date . " +{$total_days} days"));
log_to_file("fill_campaign_timeline - Start: $start_date End: $end_date");
+ // log_to_file("fill_campaign_timeline - Campaign timeline $campaign_timeline");
- // // Calculate weekly volumes
- // foreach ($campaign_timeline as $date => $data) {
- // $week = date('Y-W', strtotime($date));
- // if ($week !== $current_week) {
- // if ($current_week !== '') {
- // $weekly_volumes[$current_week] = $weekly_total;
- // }
- // $current_week = $week;
- // $weekly_total = 0;
- // }
- // $weekly_total += $data['target_volume'];
- // if (next($campaign_timeline) === false) {
- // $weekly_volumes[$week] = $weekly_total;
- // }
- // }
// Calculate weekly volumes
foreach ($campaign_timeline as $date => $data) {
+ // log_to_file("fill_campaign_timeline - Campaign timeline date: {$date}", $data);
$timestamp = strtotime($date);
$year = date('o', $timestamp); // ISO year
$week = date('W', $timestamp); // ISO week
@@ -177,14 +267,18 @@ class RL_MailWarmer_Campaign_Helper
if (!isset($weekly_volumes[$week_key])) {
$weekly_volumes[$week_key] = 0;
}
+ // log_to_file("fill_campaign_timeline - Weekly Volume: ", $weekly_volumes);
$weekly_volumes[$week_key] += $data['target_volume'];
}
+ action_log("fill_campaign_timeline - Creating multi-step conversations for campaign {$campaign_id}. Message count: {$total_message_count}");
+
// Process each week
foreach ($weekly_volumes as $week => $volume) {
log_to_file("fill_campaign_timeline - Week $week goal: $volume");
$weekly_scheduled_messages = 0;
+ // Process each conversation length (short, medium, long, etc.)
foreach ($ratios as $length => $ratio) {
// Calculate number of conversations for this length
$percent = mt_rand($ratio['percent_of_volume_lower'], $ratio['percent_of_volume_upper']) / 100;
@@ -201,6 +295,9 @@ class RL_MailWarmer_Campaign_Helper
break 2; // Break both loops
}
+ // Generate random number of responses for this conversation
+ $num_particpants = mt_rand($ratio['num_participants_lower'], $ratio['num_participants_upper']);
+
// Generate random number of responses for this conversation
$num_responses = mt_rand($ratio['num_responses_lower'], $ratio['num_responses_upper']);
@@ -213,8 +310,9 @@ class RL_MailWarmer_Campaign_Helper
}
// Generate conversation blueprint
- $conversation_steps = self::generate_conversation_blueprint(
+ $conversation_steps = RL_MailWarmer_Conversation_Handler::generate_conversation_blueprint(
$campaign_id,
+ $num_particpants,
$num_responses,
$week_start,
$end_date,
@@ -227,6 +325,7 @@ class RL_MailWarmer_Campaign_Helper
if (isset($campaign_timeline[$step_date])) {
$campaign_timeline[$step_date]['current_volume']++;
$weekly_scheduled_messages++;
+ $total_message_count++;
// Check if date is now filled
if ($campaign_timeline[$step_date]['current_volume'] >=
@@ -235,10 +334,13 @@ class RL_MailWarmer_Campaign_Helper
}
}
}
+
+ $total_conversation_count++;
}
}
}
+ action_log("fill_campaign_timeline - Creating single-step conversations for campaign {$campaign_id}. Message count: {$total_message_count}");
// log_to_file("fill_campaign_timeline - Campaign Timeline without single-step conversations: ", $campaign_timeline);
// Fill remaining capacity with single-message conversations
@@ -248,9 +350,10 @@ class RL_MailWarmer_Campaign_Helper
// log_to_file("fill_campaign_timeline - $date remaining: $remaining");
while ($remaining > 0) {
- $conversation_steps = self::generate_conversation_blueprint(
+ $conversation_steps = RL_MailWarmer_Conversation_Handler::generate_conversation_blueprint(
$campaign_id,
- 0,
+ 1,
+ 1,
$date,
$date
);
@@ -259,6 +362,7 @@ class RL_MailWarmer_Campaign_Helper
$step_date = date('Y-m-d', strtotime($step['scheduled_for']));
if (isset($campaign_timeline[$step_date])) {
$campaign_timeline[$step_date]['current_volume']++;
+ $total_message_count++;
$remaining--;
if ($campaign_timeline[$step_date]['current_volume'] >=
@@ -267,694 +371,32 @@ class RL_MailWarmer_Campaign_Helper
}
}
}
+
+ $total_conversation_count++;
}
}
- return $campaign_timeline;
+ action_log("fill_campaign_timeline - Created {$total_conversation_count} conversations with {$total_message_count} messages for campaign {$campaign_id}");
+ // return $campaign_timeline;
+ return true;
}
-
-
-
-
- /**
- * Generate a conversation blueprint for a campaign.
- *
- * @param int $campaign_id The ID of the campaign.
- * @param int $number_responses The number of responses/messages to schedule. Defaults to 0.
- * @return int The ID of the created conversation in the database.
- * @throws Exception If required data is missing or saving fails.
- */
- public static function generate_conversation_blueprint($campaign_id, $number_responses = 0, $start_date = false, $end_date = false, $filled_dates = []) {
- global $wpdb;
-
- // Fetch the start date for the campaign if one isn't passed
- if (!$start_date) {
- $start_date = get_post_meta($campaign_id, 'start_date', true);
- if (!$start_date) {
- throw new Exception(__('generate_conversation_blueprint - Campaign start date is missing.', 'rl-mailwarmer'));
- }
- }
- // log_to_file("generate_conversation_blueprint - Generating conversation with starting date: $start_date & end date: $end_date");
-
-
- // Step 1: Generate placeholders for the conversation steps
- $array_args = ['step' => '', 'status' => 'scheduled'];
- $conversation_steps = array_fill(0, $number_responses + 1, $array_args);
- $conversation_steps = self::add_timestamps_to_conversation($conversation_steps, $start_date, $end_date, $filled_dates);
- // log_to_file("generate_conversation_blueprint - Conversation Steps", $conversation_steps);
-
-
- // AI Prompt Defaults
- $args = [
- 'initiated_by' => null,
- 'received_by' => null,
- 'subject' => null,
- 'length' => null,
- 'num_participants' => null,
- 'num_responses' => 0,
- 'reply_pool' => [],
- 'cc_pool' => [],
- ];
- // $args = wp_parse_args($args, $defaults);
- $args['num_responses'] = $number_responses;
-
- // Fetch campaign target profession
- // log_to_file("generate_conversation_blueprint - Target Profession");
- $target_profession = get_field('target_profession', $campaign_id);
-
- // log_to_file("generate_conversation_blueprint - From Email");
- $from_emails = get_post_meta($campaign_id, 'email_accounts', true);
- if (!$from_emails) {
- throw new Exception(__('generate_conversation_blueprint - from_email is missing.', 'rl-mailwarmer'));
- }
- $from_pool = [];
- foreach ($from_emails as $email_id) {
- $from_pool[] = get_the_title($email_id);
- }
- // $email = get_post($from_email);
- log_to_file("generate_conversation_blueprint - From pool: ", $from_pool);
-
- // Fetch scrubber pool if 'received_by' is not passed
- // log_to_file("generate_conversation_blueprint - Scrubber Pool");
- if (!$args['received_by']) {
- $args['received_by'] = get_field('scrubber_pool', 'option') ? rl_get_textarea_meta_as_array('option', 'options_scrubber_pool') : '';
- // $args['received_by'] = explode(',', $scrubber_pool);
- }
-
- // Fetch the CC Pool
- // log_to_file("generate_conversation_blueprint - CC Pool");
- $cc_pool = self::get_email_accounts_for_pool($campaign_id, 'cc');
- // Get up to 4 random items
- $num_to_select = min(4, count($cc_pool)); // Ensure we don't request more items than are available
- $random_keys = array_rand($cc_pool, $num_to_select);
-
- // If only one item is selected, ensure it's returned as an array
- // $args['cc_pool'] = is_array($random_keys) ? array_intersect_key($cc_pool, array_flip($random_keys)) : [$cc_pool[$random_keys]];
-
-
- // log_to_file("generate_conversation_blueprint - Reply Pool");
- // Fetch the Reply pool if there will be any replies needed
- if ( intval($args['num_responses']) > 0) {
- $reply_pool = self::get_email_accounts_for_pool($campaign_id, 'reply');
- $args['reply_pool'] = array_values(array_intersect($args['received_by'], $reply_pool));
- }
-
-
-
- // log_to_file("generate_conversation_blueprint - Conversation Topics");
- // Fetch topics for the conversation if 'subject' is not passed
- if (!$args['subject']) {
- $default_topics = get_option('options_default_topic_pool') ? rl_get_textarea_meta_as_array("option", "default_topic_pool") : [];
- $campaign_topics = get_post_meta($campaign_id, 'campaign_conversation_topics', true) ? rl_get_textarea_meta_as_array($campaign_id, 'campaign_conversation_topics') : [];
- // $topics_2 = get_post_meta($campaign_id, 'campaign_conversation_topics', true);
- // log_to_file("generate_conversation_blueprint - Default topics:", $default_topics);
- // log_to_file("generate_conversation_blueprint - Campaign topics:", $campaign_topics);
- // log_to_file("generate_conversation_blueprint - Campaign topics2 : $topics_2");
-
- $all_topics = array_merge($default_topics, $campaign_topics);
- // $all_topics_count = count($all_topics);
- // log_to_file("generate_conversation - All topics $all_topics_count: ", $all_topics);
- if (!empty($all_topics)) {
- $args['subject'] = $all_topics[array_rand($all_topics)];
- } else {
- $args['subject'] = __('General Inquiry', 'rl-mailwarmer');
- }
- }
-
- // log_to_file("generate_conversation_blueprint - Prompt");
- // Generate the prompt
- $prompt = sprintf(
- "Generate a JSON email conversation with distinct participant personalities; up to 5%% errors; initiating email can be sent to multiple people; replies and follow-ups only from the sender or addresses in both 'can_reply' AND 'to_pool'. Include only: from, to, cc, subject, body. Don't include signatures Return only JSON, no notes\n%s",
- json_encode([
- 'profession' => $target_profession,
- 'from_pool' => $from_pool,
- 'to_pool' => $args['received_by'],
- 'subject' => $args['subject'],
- 'num_of_replies' => $args['num_responses'],
- 'can_reply' => $args['reply_pool'],
- 'available_to_cc' => $args['cc_pool'],
- ])
- );
-
- // log_to_file("From prompt: ", $prompt);
-
- // Step 2: Save the conversation to the database
- $conversation_data = [
- 'campaign_ID' => $campaign_id,
- 'created_at' => current_time('mysql'),
- 'status' => 'new',
- 'first_message_timestamp' => $conversation_steps[0]['scheduled_for'],
- 'prompt' => $prompt,
- 'conversation_steps' => json_encode($conversation_steps),
- ];
-
- $conversation_id = RL_MailWarmer_DB_Helper::insert_conversation($conversation_data);
- // $conversation_id = 69;
-
- // $conversation_table = $wpdb->prefix . 'rl_mailwarmer_conversation';
-
- // $wpdb->insert($conversation_table, $conversation_data);
- // $conversation_id = $wpdb->insert_id;
-
- if (!$conversation_id) {
- throw new Exception(__('generate_conversation_blueprint - Failed to save the conversation blueprint.', 'rl-mailwarmer'));
- }
-
- // Step 3: Save the individual message placeholders to the messages table
- // $message_table = $wpdb->prefix . 'rl_mailwarmer_messages';
- foreach ($conversation_steps as $step) {
- $message_data = [
- 'campaign_ID' => $campaign_id,
- 'conversation_ID' => $conversation_id,
- 'scheduled_for_timestamp' => $step['scheduled_for'],
- 'status' => 'pending',
- ];
- $message_id = RL_MailWarmer_DB_Helper::insert_message($message_data);
- if (!$message_id) {
- throw new Exception(__('generate_conversation_blueprint - Failed to save the message blueprint.', 'rl-mailwarmer'));
- }
- }
-
- return $conversation_steps;
- }
-
-
- /**
- * Generate a single conversation for a campaign
- *
- * @param int $campaign_id The ID of the campaign.
- * @param array $args Optional arguments to customize the conversation.
- * @return int|WP_Error The ID of the created conversation or WP_Error on failure.
- */
- public static function generate_conversation($campaign_id, $args = []) {
- if (empty($campaign_id)) {
- return new WP_Error('invalid_campaign', __('Campaign ID is required.', 'rl-mailwarmer'));
- }
-
- // Fetch campaign details
- $campaign = get_post($campaign_id);
- if (!$campaign || $campaign->post_type !== 'campaign') {
- return new WP_Error('invalid_campaign', __('Invalid campaign.', 'rl-mailwarmer'));
- }
-
- // Defaults
- $defaults = [
- 'initiated_by' => null,
- 'received_by' => null,
- 'subject' => null,
- 'length' => null,
- 'num_participants' => null,
- 'num_responses' => null,
- 'reply_pool' => [],
- 'cc_pool' => [],
- ];
- $args = wp_parse_args($args, $defaults);
-
- // Fetch email accounts for this campaign's domain if 'initiated_by' is not passed
- if (!$args['initiated_by']) {
- $campaign_domain = get_field('domain', $campaign_id);
- $email_accounts = get_posts([
- 'post_type' => 'email-account',
- 'meta_query' => [
- [
- 'key' => 'domain',
- 'value' => $campaign_domain->ID,
- 'compare' => '=',
- ],
- ],
- 'posts_per_page' => -1,
- 'fields' => 'ids',
- ]);
- if (empty($email_accounts)) {
- return new WP_Error('no_initiator', __('No email accounts available for the campaign domain.', 'rl-mailwarmer'));
- }
- $args['initiated_by'] = $email_accounts[array_rand($email_accounts)];
- }
-
- // fetch the email address
- // $from_id = $args['initiated_by'];
- // $from_email = get_the_title($from_id);
- // $args['initiated_by'] = $from_email;
-
- // Set length defaults for participants and responses
- if ($args['length']) {
- $length_defaults = [
- 'extra-long' => ['num_participants' => mt_rand(2, 8), 'num_responses' => mt_rand(5, 8)],
- 'long' => ['num_participants' => mt_rand(3, 6), 'num_responses' => mt_rand(4, 6)],
- 'medium' => ['num_participants' => mt_rand(3, 5), 'num_responses' => mt_rand(3, 5)],
- 'short' => ['num_participants' => mt_rand(2, 4), 'num_responses' => mt_rand(1, 3)],
- 'single' => ['num_participants' => mt_rand(2, 6), 'num_responses' => 0],
- ];
- if (isset($length_defaults[$args['length']])) {
- $defaults = $length_defaults[$args['length']];
- // log_to_file("Length defaults: ", $defaults['num_participants']);
- if (isset($args['num_participants'])) {
- unset($defaults['num_participants']);
- }
- if (isset($args['num_responses'])) {
- unset($defaults['num_responses']);
- }
- $args = array_merge($args, $length_defaults[$args['length']]);
- }
- } else {
- if (!$args['num_participants']) {
- $args['num_participants'] = 2;
- }
- if (!$args['num_responses']) {
- $args['num_responses'] = 0;
- }
- }
-
- // Fetch topics for the conversation if 'subject' is not passed
- if (!$args['subject']) {
- $default_topics = get_option('options_default_topic_pool') ? rl_get_textarea_meta_as_array("option", "default_topic_pool") : [];
- $campaign_topics = get_post_meta($campaign_id, 'campaign_conversation_topics', true) ? rl_get_textarea_meta_as_array($campaign_id, 'campaign_conversation_topics') : [];
- $all_topics = array_merge($default_topics, $campaign_topics);
- // $all_topics_count = count($all_topics);
- // log_to_file("generate_conversation - All topics $all_topics_count: ", $all_topics);
- if (!empty($all_topics)) {
- $args['subject'] = $all_topics[array_rand($all_topics)];
- } else {
- $args['subject'] = __('General Inquiry', 'rl-mailwarmer');
- }
- }
-
- // Fetch campaign duration
- $start_date = get_field('start_date', $campaign_id);
- $warmup_period = get_field('warmup_period', $campaign_id);
- $end_date = date('Ymd', strtotime("+{$warmup_period} weeks", strtotime($start_date)));
-
- // Fetch scrubber pool if 'received_by' is not passed
- if (!$args['received_by']) {
- $args['received_by'] = get_field('scrubber_pool', 'option') ? rl_get_textarea_meta_as_array('option', 'options_scrubber_pool') : '';
- // $args['received_by'] = explode(',', $scrubber_pool);
- }
-
- // Fetch the CC Pool
- $args['cc_pool'] = self::get_email_accounts_for_pool($campaign_id, 'cc');
-
- // Fetch the Reply pool if there will be any replies needed
- if ( intval($args['num_responses']) > 0) {
- $args['reply_pool'] = self::get_email_accounts_for_pool($campaign_id, 'reply');
- }
-
- // Fetch campaign target profession
- $target_profession = get_field('target_profession', $campaign_id);
-
- // Generate the prompt
- $prompt = sprintf(
- "Generate a JSON email conversation with distinct participant personalities, up to 5%% errors, and replies only from the sender or addresses in both 'can_reply' and 'to_pool'. Include: from, to, cc, subject, body\n%s",
- json_encode([
- 'profession' => $target_profession,
- 'from' => $args['initiated_by'],
- 'to_pool' => $args['received_by'],
- 'subject' => $args['subject'],
- 'num_of_replies' => $args['num_responses'],
- 'num_of_participants' => $args['num_participants'],
- 'can_reply' => $args['reply_pool'],
- 'available_to_cc' => $args['cc_pool'],
- ], JSON_PRETTY_PRINT)
- );
-
- log_to_file("generate_conversation - Prompt: $prompt");
-
- // Save the conversation to the database
- $conversation_steps = []; // Placeholder until AI generates steps
- $conversation_id = RL_MailWarmer_DB_Helper::insert_conversation($campaign_id, $conversation_steps, $prompt);
-
- if (!$conversation_id) {
- return new WP_Error('db_error', __('Failed to create conversation.', 'rl-mailwarmer'));
- }
-
- // Generate AI content for the conversation
- // $ai_response = self::clean_ai_response(self::generate_ai_conversation($prompt));
- $ai_response = self::clean_ai_response(get_post_meta($campaign_id, 'last_ai_response', true));
-
- if (is_wp_error($ai_response)) {
- return $ai_response; // Handle AI generation failure gracefully
- }
-
-
- // Update the conversation with generated steps
- self::update_conversation_steps($conversation_id, $ai_response);
-
- // log_to_file("generate_conversation - AI response: $ai_response");
- // update_post_meta($campaign_id, 'last_ai_response', wp_slash($ai_response));
-
- // Parse the AI response and save messages
- // $parsed_steps = self::parse_ai_response($ai_response);
- $array_args = ['step' => ''];
- $parsed_steps = array_fill(0, $args['num_responses'], $array_args );
-
- // Save each timestamp into the parsed steps
- $parsed_steps = self::add_timestamps_to_conversation($parsed_steps, $start_date);
-
- log_to_file("generate_conversation - parsed_steps: ", $parsed_steps);
- foreach ($parsed_steps as $index => $step) {
- if (!empty($step['scheduled_for'])) {
- $is_first_message = ($index === 0);
- if (is_array($step['to'])) {
- $step['to'] = implode(', ', $step['to']);
- }
- if (is_array($step['cc'])) {
- $step['cc'] = implode(', ', $step['cc']);
- }
- // log_to_file("generate_conversation - message body: ", $step['body']);
-
- // RL_MailWarmer_DB_Helper::insert_message(
- // $campaign_id,
- // $conversation_id,
- // $step['scheduled_for'],
- // $step['from'],
- // $step['to'],
- // $step['cc'],
- // $step['subject'],
- // $step['body'],
- // $is_first_message
- // );
- }
- }
-
-
- return $conversation_id;
- }
-
-
-
- /**
- * Generate AI content for the conversation using OpenAI's ChatGPT API.
- *
- * @param string $prompt The prompt to send to ChatGPT.
- * @return string|WP_Error The AI response or WP_Error on failure.
- */
- public static function generate_ai_conversation($prompt) {
- $api_key = getenv('OPENAI_API_KEY');
-
- if (empty($api_key)) {
- return new WP_Error('missing_api_key', __('OpenAI API key is not configured.', 'rl-mailwarmer'));
- }
-
- try {
- $client = \OpenAI::client($api_key);
- $response = $client->chat()->create([
- 'model' => 'gpt-4o',
- 'messages' => [
- ['role' => 'system', 'content' => 'You are a helpful assistant specialized in generating email conversations.'],
- ['role' => 'user', 'content' => $prompt],
- ],
- 'temperature' => 0.7,
- 'max_tokens' => 2000,
- ]);
-
- return $response['choices'][0]['message']['content'];
- } catch (\Exception $e) {
- return new WP_Error('api_error', $e->getMessage());
- }
- }
-
- /**
- * Clean the AI response by removing Markdown-style code block delimiters.
- *
- * @param string $response The raw AI response.
- * @return string The cleaned JSON string.
- */
- public static function clean_ai_response($response) {
- // Use regex to remove the ```json and ``` delimiters
- return preg_replace('/^```json\s*|```$/m', '', trim($response));
- }
-
/**
* Parse the AI response into structured steps for message creation.
*
* @param string $ai_response The JSON string returned by ChatGPT.
* @return array An array of parsed conversation steps.
*/
- public static function parse_ai_response($ai_response) {
- $steps = json_decode($ai_response, true);
+ // public static function parse_ai_response($ai_response) {
+ // $steps = json_decode($ai_response, true);
- if (json_last_error() !== JSON_ERROR_NONE) {
- return new WP_Error('invalid_response', __('Failed to parse AI response.', 'rl-mailwarmer'));
- }
-
- return $steps;
- }
-
- /**
- * Fetch all published email accounts matching the campaign domain with "include_in_cc_pool" set to true.
- *
- * @param int $campaign_id The ID of the campaign.
- * @param string $pool The name of the pool. Valid options are: cc, reply
- * @return array The email addresses to include in the CC pool.
- */
- private static function get_email_accounts_for_pool($campaign_id, $pool) {
- // Get the domain associated with the campaign
- $domain_id = get_post_meta($campaign_id, 'domain', true);
-
- if (empty($domain_id)) {
- return []; // Return an empty array if no domain is found
- }
-
- // Query email accounts
- if ($pool === 'reply') {
- $args = [
- 'post_type' => 'email-account',
- 'post_status' => 'publish',
- 'meta_query' => [
- 'relation' => 'AND',
- [
- 'key' => 'include_in_' . $pool . '_pool',
- 'value' => true,
- 'compare' => '=',
- ],
- // [
- // 'key' => 'domain',
- // 'value' => $domain_id,
- // 'compare' => '=',
- // ],
- ],
- 'fields' => 'ids', // Retrieve only IDs for efficiency
- 'posts_per_page' => -1, // Fetch all matching posts
- ];
- } else if ($pool === 'cc') {
-
- $args = [
- 'post_type' => 'email-account',
- 'post_status' => 'publish',
- 'meta_query' => [
- 'relation' => 'AND',
- [
- 'key' => 'include_in_' . $pool . '_pool',
- 'value' => true,
- 'compare' => '=',
- ],
- // [
- // 'key' => 'domain',
- // 'value' => $domain_id,
- // 'compare' => '=',
- // ],
- ],
- 'fields' => 'ids', // Retrieve only IDs for efficiency
- 'posts_per_page' => -1, // Fetch all matching posts
- ];
- }
-
- $query = new WP_Query($args);
-
- $email_pool = [];
-
- if ($query->have_posts()) {
- foreach ($query->posts as $post_id) {
- $email_address = get_the_title($post_id);
- if (!empty($email_address)) {
- $email_pool[] = $email_address;
- }
- }
- }
-
- return $email_pool;
- }
-
- /**
- * Add timestamps to parsed conversation steps.
- *
- * @param array $parsed_steps The parsed steps of the conversation.
- * @param string $start_date The starting date for the timestamps.
- * @return array The updated conversation steps with timestamps and step indices.
- */
- public static function add_timestamps_to_conversation($parsed_steps, $start_date, $end_date = NULL, $filled_dates = []) {
- // Ensure parsed_steps is an array
- if (!is_array($parsed_steps) || empty($parsed_steps)) {
- throw new Exception(__('Parsed steps must be a non-empty array.', 'rl-mailwarmer'));
- }
-
- // Initialize the updated steps array
- $updated_steps = [];
-
- // Iterate through each step and assign timestamps
- foreach ($parsed_steps as $index => $step) {
-
- // If this is not the first step, ensure the start_date is at least 5 minutes after the previous step
- if ($index > 0) {
- $previous_timestamp = strtotime($updated_steps[$index - 1]['scheduled_for']);
- $start_date = date('Y-m-d H:i:s', max($timestamp, $previous_timestamp + 300)); // 300 seconds = 5 minutes
- }
-
- // Use the helper function to generate a random timestamp
- $timestamp = self::generate_random_date_time($start_date, $end_date, $filled_dates );
- // log_to_file("add_timestamps_to_conversation - Returned random timestamp: $timestamp");
-
- // Update the step with the scheduled timestamp and step index
- $step['scheduled_for'] = date('Y-m-d H:i:s', $timestamp);
- $step['step'] = $index + 1;
-
- // Update the starting date for the next iteration
- $start_date = date('Y-m-d H:i:s', $timestamp);
-
- // Append the updated step
- $updated_steps[] = $step;
- }
- // log_to_file("add_timestamps_to_conversation - Updated steps: ", $updated_steps);
-
- return $updated_steps;
- }
-
- /**
- * Generate a semi-randomized timestamp with a random date and time.
- *
- * @param string $start_date The start date in 'Y-m-d' format. Defaults to today.
- * @param string $end_date The end date in 'Y-m-d' format. Defaults to today + 7 days.
- * @param string $start_time The start time in 'H:i' format (e.g., '08:00'). Defaults to '08:00'.
- * @param string $end_time The end time in 'H:i' format (e.g., '18:00'). Defaults to '18:00'.
- * @return int The random timestamp.
- */
- private static function generate_random_date_time($start_date = null, $end_date = null, $filled_dates = [], $start_time = '07:20', $end_time = '21:00' ) {
- // Default dates
- $start_date = $start_date ?: date('Y-m-d H:i');
- $end_date = $end_date ?: date('Y-m-d H:i', strtotime($start_date . ' +8 days'));
-
- // log_to_file("generate_random_date_time - Generating random date time between $start_date & $end_date");
-
- $start_date_timestamp = strtotime($start_date);
- $end_date_timestamp = strtotime($end_date);
- $retry = false;
-
- if ($end_date_timestamp < $start_date_timestamp) {
-
- throw new Exception('generate_random_date_time - End date must be greater than or equal to start date.');
-
- } else if ( $end_date_timestamp === $start_date_timestamp ) {
-
- // Single-part conversations have the same start & end date
-
- $random_date = date('Y-m-d', $start_date_timestamp);
- // log_to_file("generate_random_date_time - Using the same start & end date");
-
- } else {
-
- // Generate a random date within the timeframe
- // log_to_file("generate_random_date_time - Finding a date that's not in \$filled_dates");
- do {
-
- // if ($retry) {
- // // log_to_file("generate_random_date_time - Regenerating random date because it was found in filled_dates.");
-
- // }
-
- // Generate a random date between the start and end dates
- $random_date_timestamp = mt_rand($start_date_timestamp, $end_date_timestamp);
- $random_date = date('Y-m-d', $random_date_timestamp);
-
- // $retry = true;
- // log_to_file("generate_random_date_time - Random date $random_date");
- // log_to_file("generate_random_date_time - Filled Dates: ", $filled_dates);
- } while (in_array($random_date, $filled_dates));
- // log_to_file("generate_random_date_time - Random date found!");
- }
-
-
-
-
- // Generate a random time within the specified range for the selected date
- // log_to_file("generate_random_date_time - calling generate_random_time( $random_date, $start_time, $end_time)");
- $random_time_timestamp = self::generate_random_time($random_date, $start_time, $end_time);
- // log_to_file("generate_random_date_time - Returned random timestamp: $random_time_timestamp");
-
- return $random_time_timestamp;
- }
-
- /**
- * Generate a random timestamp for a given date between specific hours.
- *
- * @param string $date The date in 'Y-m-d' format.
- * @param string $start_time The start time in 'H:i' format.
- * @param string $end_time The end time in 'H:i' format.
- * @return int The random timestamp.
- */
- private static function generate_random_time($date, $start_time, $end_time) {
- // Convert start and end times to timestamps
- $start_timestamp = strtotime("$date $start_time");
- $end_timestamp = strtotime("$date $end_time");
- // log_to_file("generate_random_time - Start: $start_timestamp End: $end_timestamp");
-
- if ($end_timestamp < $start_timestamp) {
- throw new Exception('generate_random_time - End time must be greater or equal to start time.');
- }
-
- // Generate a random timestamp within the time range
- $timestamp = mt_rand($start_timestamp, $end_timestamp);
- // log_to_file("generate_random_time - Random Timestamp: $timestamp");
- return $timestamp;
- }
-
-
-
-
-
- /**
- * Update the conversation steps for a given conversation ID.
- *
- * @param int $conversation_id The ID of the conversation to update.
- * @param string $ai_response The AI-generated response in JSON format.
- * @return bool|WP_Error True on success, WP_Error on failure.
- */
- public static function update_conversation_steps($conversation_id, $ai_response) {
- global $wpdb;
- // log_to_file("update_conversation_steps - Running! $conversation_id ", $ai_response);
-
- // Validate input
- if (empty($conversation_id) || !is_int($conversation_id)) {
- return new WP_Error('invalid_conversation_id', __('Invalid conversation ID.', 'rl-mailwarmer'));
- }
-
- if (empty($ai_response) || !is_string($ai_response)) {
- return new WP_Error('invalid_ai_response', __('Invalid AI response.', 'rl-mailwarmer'));
- }
-
- // Define the table name
- $table_name = $wpdb->prefix . 'rl_mailwarmer_conversation';
-
- // Prepare the data
- $data = [
- 'conversation_steps' => $ai_response, // Save the JSON response as a string
- ];
- $where = [
- 'id' => $conversation_id,
- ];
-
- // Update the database
- $result = $wpdb->update($table_name, $data, $where, ['%s'], ['%d']);
-
- if ($result === false) {
- return new WP_Error('db_update_failed', __('Failed to update the conversation steps.', 'rl-mailwarmer'));
- }
- // log_to_file("update_conversation_steps - Updated conversation steps for #$conversation_id");
- return true;
- }
-
-
+ // if (json_last_error() !== JSON_ERROR_NONE) {
+ // return new WP_Error('invalid_response', __('Failed to parse AI response.', 'rl-mailwarmer'));
+ // }
+ // return $steps;
+ // }
@@ -964,6 +406,14 @@ class RL_MailWarmer_Campaign_Helper
* Add a meta box for generating the campaign timeline.
*/
add_action('add_meta_boxes', function () {
+ add_meta_box(
+ 'show_campaign_tracking_id',
+ __('Tracking ID', 'rl-mailwarmer'),
+ 'rl_mailwarmer_render_tracking_id_box',
+ 'campaign',
+ 'side',
+ 'default'
+ );
add_meta_box(
'generate_campaign_timeline',
__('Generate Timeline', 'rl-mailwarmer'),
@@ -974,6 +424,22 @@ add_action('add_meta_boxes', function () {
);
});
+
+
+
+/**
+ * Render the "Tracking ID" meta box.
+ *
+ * @param WP_Post $post The current post object.
+ */
+function rl_mailwarmer_render_tracking_id_box($post)
+{
+ $campaign_tracking_id = get_field('campaign_tracking_id', $post->ID);
+ ?>
+
+ getMessage());
}
@@ -1248,13 +714,13 @@ function rl_mailwarmer_display_campaign_timeline($post) {
}
// Fetch campaign timeline
- $campaign_timeline = get_post_meta($post->ID, 'campaign_timeline', true);
+ $campaign_timeline = get_post_meta($post->ID, 'campaign_timeline_json', true);
if (empty($campaign_timeline)) {
echo '
' . esc_html__('No timeline available for this campaign.', 'rl-mailwarmer') . '
';
return;
}
- $campaign_timeline = json_decode($campaign_timeline, true);
+ $campaign_timeline = $campaign_timeline;
// Organize timeline by weeks for grid structure
$weeks = [];
@@ -1342,6 +808,7 @@ function rl_mailwarmer_display_campaign_timeline($post) {
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'));
+ $start_time = date('Y-m-d H:i:s', strtotime('-96 hours'));
+ $end_time = date('Y-m-d H:i:s', strtotime('+12 hours'));
+ // $end_time = date('Y-m-d H:i:s', strtotime('now'));
// 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",
+ "SELECT * FROM $conversation_table WHERE status = %s AND first_message_timestamp BETWEEN %s AND %s ORDER BY first_message_timestamp ASC LIMIT 50",
'new',
- $current_time,
- $future_time
+ $start_time,
+ $end_time
),
ARRAY_A
);
- // log_to_file("process_upcoming_conversations - Conversations: ", $conversations);
+ $conversations_count = count($conversations);
+
+ if ($conversations_count > 0) {
+ log_to_file("process_upcoming_conversations - Processing {$conversations_count} conversations" );
+ action_log("process_upcoming_conversations - Processing {$conversations_count} conversations" );
+ foreach ($conversations as $conversation) {
+ log_to_file("process_upcoming_conversations - Conversation: ", $conversation['id']);
+ try {
+ // Generate conversation content using AI
+ // $ai_response = self::fetch_conversation_from_ai($conversation);
+ $ai_response = isset($conversation['ai_response']) ? json_decode($conversation['ai_response'], true) : false;
+ $prompt = $conversation['prompt'];
+ $model = $conversation['model'];
+
+ // 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));
+ if ($ai_response) {
+ log_to_file("process_upcoming_conversations - Using cached AI Response");
+ } else {
+ log_to_file("process_upcoming_conversations - No cached AI Response found. Fetching.");
+ $raw_ai_response = self::generate_ai_conversation($prompt, $model);
+ // log_to_file("process_upcoming_conversations - AI Reponse: {$raw_ai_response}");
+ // $ai_response_type = gettype($raw_ai_response);
+ // log_to_file("process_upcoming_conversations - AI Reponse is a : ", $ai_response_type);
- 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'];
+ $ai_response = self::normalize_conversation_format($raw_ai_response);
- // log_to_file("process_upcoming_conversations - AI prompt: ", $prompt);
+ // Save the generated content to the conversation
+ $update_data = [
+ 'ai_response' => json_encode($ai_response),
+ ];
- // $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);
+ $update_db_with_AI_response = RL_MailWarmer_DB_Helper::update_conversation(intval($conversation['id']), $update_data);
+ // log_to_file("process_upcoming_conversations - update_db_with_AI_response: ", $update_db_with_AI_response);
- if (is_wp_error($ai_response)) {
- return $ai_response; // Handle AI generation failure gracefully
+ }
+ // $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)) {
+ log_to_file("process_upcoming_conversations - AI Response is an error!");
+ return $ai_response; // Handle AI generation failure gracefully
+ }
+
+ log_to_file("process_upcoming_conversations - Before merge_timeline_with_ai_response");
+ $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_conversations';
+ $status = 'generated';
+
+ // log_to_file("process_upcoming_conversations - Updating DB for conversation {$conversation['id']} to status: {$status}. Response: {$ai_response} New steps: ", $updated_conversation_steps);
+ $result = $wpdb->update(
+ $conversation_table,
+ // [],
+ ['status' => $status, 'conversation_steps' => json_encode($updated_conversation_steps), 'ai_response' => json_encode($ai_response)],
+ ['id' => $conversation['id']],
+ ['%s'],
+ ['%s'],
+ ['%s'],
+ ['%d']
+ );
+ // log_to_file("process_upcoming_conversations - Update DB Result: ", $result);
+
+ $update_messages_db = self::update_messages_with_merged_output($conversation['id'], json_encode($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 (self::update_conversation_steps($conversation['id'], $updated_conversation_steps)) {
+ log_to_file("process_upcoming_conversations - Updated conversation_steps!");
+ }
+
+
+
+
+ } catch (Exception $e) {
+ error_log("Failed to generate conversation ID {$conversation['id']}: " . $e->getMessage());
}
-
- $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());
}
+ action_log("process_upcoming_conversations - Finished processing {$conversations_count} conversations");
+ return $conversations_count;
+ } else {
+ return 0;
}
}
private static function update_messages_with_merged_output($conversation_id, $merged_output) {
- log_to_file("update_messages_with_merged_output - merged_output: ", $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);
+ $merged_output = json_decode($merged_output);
+ $merged_output_length = count($merged_output);
+ // log_to_file("update_messages_with_merged_output - merged_output length: ", $merged_output_length);
// Fetch all messages for the given conversation ID
$messages = $wpdb->get_results(
@@ -96,7 +132,8 @@ class RL_MailWarmer_Conversation_Handler {
),
ARRAY_A
);
- log_to_file("update_messages_with_merged_output - Found Messages: ", $messages);
+ $messages_length = count($messages);
+ // log_to_file("update_messages_with_merged_output - Found {$messages_length} messages: ", $messages);
if (empty($messages)) {
throw new Exception("No messages found for conversation ID $conversation_id.");
@@ -105,103 +142,834 @@ class RL_MailWarmer_Conversation_Handler {
// Iterate through messages and merged output
foreach ($messages as $index => $message) {
if (!isset($merged_output[$index])) {
+ // log_to_file("update_messages_with_merged_output - No matching message & output found. Skipping! {$index} ");
continue; // Skip if there's no corresponding merged output
+ } else {
+ // log_to_file("update_messages_with_merged_output - Processing message {$index} ");
}
+ // $merged = json_decode($merged_output[$index], true);
$merged = $merged_output[$index];
- // log_to_file("update_messages_with_merged_output - merged: $merged");
+ // log_to_file("update_messages_with_merged_output - Message Data:", $merged);
+ // $merged_length = count($merged);
+ // log_to_file("update_messages_with_merged_output - Merged {$merged_length}:", $merged);
+ // log_to_file("update_messages_with_merged_output - merged[status]:", $merged->status);
// 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'],
- // ];
+ // $updated_data = [];
+ $updated_data = [
+ 'status' => $merged->status,
+ 'scheduled_for_timestamp' => $merged->scheduled_for,
+ 'from_email' => $merged->from,
+ 'to_email' => json_encode($merged->to),
+ 'cc' => 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
- // );
+ $db_update_message_result = $wpdb->update(
+ $message_table,
+ $updated_data,
+ ['id' => intval($message['id'])], // Ensure the correct message ID is passed here
+ ['%s', '%s', '%s', '%s', '%s', '%s', '%s'], // Format specifiers
+ ['%d']
+ );
+ if ($db_update_message_result) {
+ log_to_file("update_messages_with_merged_output - DB Update result: ", $db_update_message_result);
+
+ } else {
+ $last_query = $wpdb->last_query;
+ log_to_file("update_messages_with_merged_output - Problem updating DB: ", $last_query);
+ return false;
+ }
+ }
+ return true;
+
+ }
+
+ public static function merge_timeline_with_ai_response($conversation_string, $ai_response) {
+ $conversation = json_decode($conversation_string, true);
+ $conversation_type = gettype($conversation);
+ $ai_response_type = gettype($ai_response);
+
+ // log_to_file("merge_timeline_with_ai_response - Conversation type: ", $conversation_type);
+ // log_to_file("merge_timeline_with_ai_response - AI Response type: ", $ai_response_type);
+ // log_to_file("merge_timeline_with_ai_response - Conversation: ", $conversation);
+ // log_to_file("merge_timeline_with_ai_response - AI Response: ", $ai_response);
+
+ // Ensure both arrays have the same number of elements
+ // log_to_file("merge_timeline_with_ai_response - Counting arrays");
+ $merged = [];
+ $count_timeline = count($conversation);
+ $count_ai_response = count($ai_response);
+ // log_to_file("merge_timeline_with_ai_response - Timeline Count: {$count_timeline} AI Response Count: {$count_ai_response}");
+
+ if ($count_ai_response >= $count_timeline) {
+ // Trim the AI responses to match the timeline count
+ // log_to_file("merge_timeline_with_ai_response - Trimming AI messages to merge with conversation");
+ $ai_response = array_slice($ai_response, 0, $count_timeline);
+ } elseif ($count_ai_response < $count_timeline) {
+ // log_to_file("merge_timeline_with_ai_response - AI response is too short to merge with conversation");
+ throw new Exception('The number of AI responses is less than the timeline steps.');
}
+ // log_to_file("merge_timeline_with_ai_response - Attempting merge");
+ // Merge corresponding elements
+ foreach ($conversation as $index => $timeline_step) {
+ $ai_message = $ai_response[$index];
+ $timeline_step_type = gettype($timeline_step);
+ $ai_message_type = gettype($ai_message);
+
+
+ // log_to_file("merge_timeline_with_ai_response - Conversation step type: ", $timeline_step_type);
+ // log_to_file("merge_timeline_with_ai_response - AI Message type: ", $ai_message_type);
+
+ $merge_result = array_merge($timeline_step, $ai_message);
+ // log_to_file("merge_timeline_with_ai_response - Merged: ", $merge_result);
+ $merged[] = $merge_result;
+ }
+
+
+ // log_to_file("merge_timeline_with_ai_response - Merged Steps: ", $merged);
+
+ // Return the merged result as JSON
+ return $merged;
+ }
+
+
+ /**
+ * Update the conversation steps for a given conversation ID.
+ *
+ * @param int $conversation_id The ID of the conversation to update.
+ * @param string $ai_response The AI-generated response in JSON format.
+ * @return bool|WP_Error True on success, WP_Error on failure.
+ */
+ public static function update_conversation_steps($conversation_id, $ai_response) {
+ global $wpdb;
+ // log_to_file("update_conversation_steps - Running! $conversation_id ", $ai_response);
+
+ // Validate input
+ if (empty($conversation_id) || !is_int($conversation_id)) {
+ return new WP_Error('invalid_conversation_id', __('Invalid conversation ID.', 'rl-mailwarmer'));
+ }
+
+ if (empty($ai_response) || !is_string($ai_response)) {
+ return new WP_Error('invalid_ai_response', __('Invalid AI response.', 'rl-mailwarmer'));
+ }
+
+ // Define the table name
+ $table_name = $wpdb->prefix . 'rl_mailwarmer_conversation';
+
+ // Prepare the data
+ $data = [
+ 'conversation_steps' => $ai_response, // Save the JSON response as a string
+ ];
+ $where = [
+ 'id' => $conversation_id,
+ ];
+
+ // Update the database
+ $result = $wpdb->update($table_name, $data, $where, ['%s'], ['%d']);
+
+ if ($result === false) {
+ return new WP_Error('db_update_failed', __('Failed to update the conversation steps.', 'rl-mailwarmer'));
+ }
+ // log_to_file("update_conversation_steps - Updated conversation steps for #$conversation_id");
return true;
}
/**
- * Fetch a conversation from ChatGPT based on conversation parameters.
+ * Generate a conversation blueprint for a campaign.
*
- * @param array $conversation The conversation data.
- * @return string The AI-generated conversation as JSON.
- * @throws Exception If the API call fails.
+ * @param int $campaign_id The ID of the campaign.
+ * @param int $number_responses The number of responses/messages to schedule. Defaults to 0.
+ * @return int The ID of the created conversation in the database.
+ * @throws Exception If required data is missing or saving 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'])))
+ public static function generate_conversation_blueprint($campaign_id, $num_particpants = 1, $number_responses = 1, $start_date = false, $end_date = false, $filled_dates = []) {
+ global $wpdb;
+
+ // log_to_file("generate_conversation_blueprint - {$campaign_id} {$number_responses} {$start_date} {$end_date}", $filled_dates);
+
+ $campaign_limited = get_post_meta($campaign_id, 'campaign_limited', true);
+
+
+ // Fetch the start date for the campaign if one isn't passed
+ if (!$start_date) {
+ $start_date = get_post_meta($campaign_id, 'start_date', true);
+ if (!$start_date) {
+ throw new Exception(__('generate_conversation_blueprint - Campaign start date is missing.', 'rl-mailwarmer'));
+ }
+ }
+ // log_to_file("generate_conversation_blueprint - Generating conversation with starting date: $start_date & end date: $end_date");
+
+
+ // Step 1: Generate placeholders for the conversation steps
+ $array_args = ['step' => '', 'status' => 'scheduled'];
+ $conversation_steps_placeholder = array_fill(0, $number_responses, $array_args);
+ // log_to_file("generate_conversation_blueprint - Conversation Steps Placeholder", $conversation_steps_placeholder);
+
+ $conversation_steps = self::add_timestamps_to_conversation($conversation_steps_placeholder, $start_date, $end_date, $filled_dates);
+ // log_to_file("generate_conversation_blueprint - Conversation Steps", $conversation_steps);
+
+
+ // AI Prompt Defaults
+ $args = [
+ 'initiated_by' => null,
+ 'received_by' => null,
+ 'subject' => null,
+ 'length' => null,
+ 'num_participants' => $num_particpants,
+ 'num_responses' => $number_responses,
+ 'reply_pool' => [],
+ 'cc_pool' => [],
];
+ // $args = wp_parse_args($args, $defaults);
+ // $args['num_responses'] = $number_responses;
+
+ $campaign_tracking_id = get_post_meta($campaign_id, 'campaign_tracking_id', true);
- // Call OpenAI API
- $response = RL_MailWarmer_Api_Helper::call_openai_api($prompt);
+ // Fetch campaign target profession
+ // log_to_file("generate_conversation_blueprint - Target Profession");
+ $target_profession = get_field('target_profession', $campaign_id);
- if (!$response || !isset($response['choices'][0]['text'])) {
- throw new Exception('Invalid response from OpenAI API');
+ $from_emails = get_post_meta($campaign_id, 'email_accounts', true);
+
+ log_to_file("generate_conversation_blueprint - From Emails", $from_emails);
+ if (!$from_emails) {
+ throw new Exception(__('generate_conversation_blueprint - from_email is missing.', 'rl-mailwarmer'));
+ }
+ $from_emails_key = array_rand($from_emails, 1);
+ $from_address = get_the_title($from_emails[$from_emails_key]);
+ // log_to_file("generate_conversation_blueprint - From Email ID", $from_emails[$from_emails_key]);
+ // log_to_file("generate_conversation_blueprint - From Email Address", $from_address);
+ // $from_pool = [];
+ // foreach ($from_emails as $email_id) {
+ // }
+ // $email = get_post($from_email);
+ // log_to_file("generate_conversation_blueprint - From pool: ", $from_pool);
+
+ // Fetch scrubber pool if 'received_by' is not passed
+ if (!isset($args['received_by'])) {
+ $scrubber_pool_full = self::get_email_accounts_for_pool($campaign_id, 'scrubber_test');
+
+ // Pick $num_particpants random addresses
+ $num_scrubbers = min($num_particpants, count($scrubber_pool_full));
+ $scrubber_pool = array_rand(array_flip($scrubber_pool_full), $num_scrubbers);
+
+
+ // Ensure $random_keys is an array
+ if ($num_scrubbers === 1) {
+ $args['received_by'] = [$scrubber_pool];
+ } else {
+ $args['received_by'] = $scrubber_pool;
+ }
+
+ log_to_file("generate_conversation_blueprint - Scrubber Pool: ", $args['received_by']);
}
- return $response['choices'][0]['text'];
+ // Fetch the CC Pool
+ // log_to_file("generate_conversation_blueprint - CC Pool");
+ // $cc_pool = self::get_email_accounts_for_pool($campaign_id, 'cc');
+ // // Get up to 4 random items
+ // $num_to_select = min(4, count($cc_pool)); // Ensure we don't request more items than are available
+ // $random_keys = array_rand($cc_pool, $num_to_select);
+
+ // If only one item is selected, ensure it's returned as an array
+ // $args['cc_pool'] = is_array($random_keys) ? array_intersect_key($cc_pool, array_flip($random_keys)) : [$cc_pool[$random_keys]];
+
+
+ // log_to_file("generate_conversation_blueprint - Reply Pool");
+ // Fetch the Reply pool if there will be any replies needed
+ if ( intval($args['num_responses']) > 0) {
+ $reply_pool = self::get_email_accounts_for_pool($campaign_id, 'reply');
+ $args['reply_pool'] = array_values(array_intersect($args['received_by'], $reply_pool));
+ log_to_file("generate_conversation_blueprint - Reply Pool: ", $args['reply_pool']);
+ }
+
+
+
+ // log_to_file("generate_conversation_blueprint - Conversation Topics");
+ // Fetch topics for the conversation if 'subject' is not passed
+ if (!$args['subject']) {
+ $default_topics = get_option('options_default_topic_pool') ? rl_get_textarea_meta_as_array("option", "default_topic_pool") : [];
+ $campaign_topics = get_post_meta($campaign_id, 'campaign_conversation_topics', true) ? rl_get_textarea_meta_as_array($campaign_id, 'campaign_conversation_topics') : [];
+ // $topics_2 = get_post_meta($campaign_id, 'campaign_conversation_topics', true);
+ // log_to_file("generate_conversation_blueprint - Default topics:", $default_topics);
+ // log_to_file("generate_conversation_blueprint - Campaign topics:", $campaign_topics);
+ // log_to_file("generate_conversation_blueprint - Campaign topics2 : $topics_2");
+
+ $all_topics = array_merge($default_topics, $campaign_topics);
+ // $all_topics_count = count($all_topics);
+ // log_to_file("generate_conversation - All topics $all_topics_count: ", $all_topics);
+ if (!empty($all_topics)) {
+ $args['subject'] = $all_topics[array_rand($all_topics)];
+ } else {
+ $args['subject'] = __('General Inquiry', 'rl-mailwarmer');
+ }
+ }
+
+ log_to_file("generate_conversation_blueprint - Conversation args", $args);
+ // Generate the prompt
+ if ($campaign_limited || (intval($args['num_responses']) === 1) ) {
+ $chat_model = 'gpt-4o-mini';
+ // $chat_model = 'o3-mini';
+ $prompt = sprintf(
+ "Generate a single JSON email from {$from_address} to one or more addresses in 'to_pool' with no replies. Fields to return: from, to, cc, subject, body, valediction. The valediction should be separate from the body. Return only JSON, no notes\n%s",
+ json_encode([
+ 'profession' => $target_profession,
+ 'from_address' => $from_address,
+ 'to_pool' => $args['received_by'],
+ 'subject' => $args['subject'],
+ 'num_of_replies' => 1,
+ 'available_to_cc' => $args['cc_pool'],
+ ])
+ );
+
+ } else {
+ $chat_model = 'gpt-4o';
+ // $chat_model = 'o3-mini';
+ $num_replies = intval($args['num_responses']) + 1;
+ $prompt = sprintf(
+ "Generate a JSON email conversation with distinct participant personalities; up to 5%% errors; initiating email can be sent to multiple people; replies and follow-ups only from the sender or addresses in both 'can_reply' AND 'to_pool'. Fields to return: from, to, cc, subject, body, valediction. The valediction should be separate from the body. Return only JSON, no notes\n%s",
+ json_encode([
+ 'profession' => $target_profession,
+ 'from_address' => $from_address,
+ 'to_pool' => $args['received_by'],
+ 'subject' => $args['subject'],
+ 'num_of_replies' => $num_replies,
+ 'can_reply' => $args['reply_pool'],
+ 'available_to_cc' => $args['cc_pool'],
+ ])
+ );
+ }
+
+
+ // log_to_file("From prompt: ", $prompt);
+
+ // Step 2: Save the conversation to the database
+ $conversation_data = [
+ 'campaign_ID' => $campaign_id,
+ 'email_account_id' => $from_emails[$from_emails_key],
+ 'created_at' => current_time('mysql'),
+ 'status' => 'new',
+ 'first_message_timestamp' => $conversation_steps[0]['scheduled_for'],
+ 'prompt' => $prompt,
+ 'model' => $chat_model,
+ 'conversation_steps' => json_encode($conversation_steps),
+ 'campaign_tracking_id' => $campaign_tracking_id,
+ ];
+ // log_to_file("generate_conversation_blueprint - Conversation Data: ", $conversation_data);
+
+ $conversation_id = RL_MailWarmer_DB_Helper::insert_conversation($conversation_data);
+ // $conversation_id = 69;
+
+ // $conversation_table = $wpdb->prefix . 'rl_mailwarmer_conversation';
+
+ // $wpdb->insert($conversation_table, $conversation_data);
+ // $conversation_id = $wpdb->insert_id;
+
+ if (!$conversation_id) {
+ throw new Exception(__('generate_conversation_blueprint - Failed to save the conversation blueprint.', 'rl-mailwarmer'));
+ }
+
+ // Step 3: Save the individual message placeholders to the messages table
+ // $message_table = $wpdb->prefix . 'rl_mailwarmer_messages';
+ $previous_message_id = null; // Initialize the previous message ID
+ foreach ($conversation_steps as $index => $step) {
+ $is_first_message = $index === 0 ? 1 : 0;
+ $message_data = [
+ 'campaign_ID' => $campaign_id,
+ 'campaign_tracking_id' => $campaign_tracking_id . '-' .$conversation_id,
+ 'email_account_id' => $from_emails[$from_emails_key],
+ 'conversation_ID' => $conversation_id,
+ 'scheduled_for_timestamp' => $step['scheduled_for'],
+ 'status' => 'new',
+ 'first_message' => $is_first_message,
+ 'previous_message_id' => $previous_message_id, // Include previous message ID here
+ ];
+ log_to_file("generate_conversation_blueprint - Message Data: ", $message_data);
+ $message_id = RL_MailWarmer_DB_Helper::insert_message($message_data);
+ if (!$message_id) {
+ throw new Exception(__('generate_conversation_blueprint - Failed to save the message blueprint.', 'rl-mailwarmer'));
+ }
+ // Save the message_id back to the current step
+ $conversation_steps[$index]['message_id'] = $message_id;
+
+ // Update the previous_message_id for the next iteration
+ $previous_message_id = $message_id;
+ }
+
+
+ // Update the conversation with the message IDS
+ $update_data['conversation_steps'] = json_encode($conversation_steps);
+ // log_to_file("generate_conversation_blueprint - Update Data: ", $update_data);
+ RL_MailWarmer_DB_Helper::update_conversation($conversation_id, $update_data);
+
+ return $conversation_steps;
}
- 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.');
+ /**
+ * Generate a single conversation for a campaign - On the admin page sidebar
+ *
+ * @param int $campaign_id The ID of the campaign.
+ * @param array $args Optional arguments to customize the conversation.
+ * @return int|WP_Error The ID of the created conversation or WP_Error on failure.
+ */
+ public static function generate_conversation($campaign_id, $args = []) {
+ if (empty($campaign_id)) {
+ return new WP_Error('invalid_campaign', __('Campaign ID is required.', 'rl-mailwarmer'));
}
- // Merge corresponding elements
- foreach ($conversation as $index => $timeline_step) {
- $ai_message = $ai_response[$index];
- $merged[] = array_merge($timeline_step, $ai_message);
+ // Fetch campaign details
+ $campaign = get_post($campaign_id);
+ if (!$campaign || $campaign->post_type !== 'campaign') {
+ return new WP_Error('invalid_campaign', __('Invalid campaign.', 'rl-mailwarmer'));
}
- // Return the merged result as JSON
- return json_encode($merged, JSON_PRETTY_PRINT);
+ // Defaults
+ $defaults = [
+ 'initiated_by' => null,
+ 'received_by' => null,
+ 'subject' => null,
+ 'length' => null,
+ 'num_participants' => null,
+ 'num_responses' => null,
+ 'reply_pool' => [],
+ 'cc_pool' => [],
+ ];
+ $args = wp_parse_args($args, $defaults);
+
+ // Fetch email accounts for this campaign's domain if 'initiated_by' is not passed
+ if (!$args['initiated_by']) {
+ $campaign_domain = get_field('domain', $campaign_id);
+ $email_accounts = get_posts([
+ 'post_type' => 'email-account',
+ 'meta_query' => [
+ [
+ 'key' => 'domain',
+ 'value' => $campaign_domain->ID,
+ 'compare' => '=',
+ ],
+ ],
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ ]);
+ if (empty($email_accounts)) {
+ return new WP_Error('no_initiator', __('No email accounts available for the campaign domain.', 'rl-mailwarmer'));
+ }
+ $args['initiated_by'] = $email_accounts[array_rand($email_accounts)];
+ }
+
+ if ( (int) $args['length'] > 2 ) {
+ $chat_model = 'gpt-4o';
+ } else {
+ $chat_model = 'gpt-4o-mini';
+ }
+
+ // fetch the email address
+ // $from_id = $args['initiated_by'];
+ // $from_email = get_the_title($from_id);
+ // $args['initiated_by'] = $from_email;
+
+ // Set length defaults for participants and responses
+ if ($args['length']) {
+ $length_defaults = [
+ 'extra-long' => ['num_participants' => mt_rand(2, 8), 'num_responses' => mt_rand(5, 8)],
+ 'long' => ['num_participants' => mt_rand(3, 6), 'num_responses' => mt_rand(4, 6)],
+ 'medium' => ['num_participants' => mt_rand(3, 5), 'num_responses' => mt_rand(3, 5)],
+ 'short' => ['num_participants' => mt_rand(2, 4), 'num_responses' => mt_rand(1, 3)],
+ 'single' => ['num_participants' => mt_rand(2, 6), 'num_responses' => 0],
+ ];
+ if (isset($length_defaults[$args['length']])) {
+ $defaults = $length_defaults[$args['length']];
+ // log_to_file("Length defaults: ", $defaults['num_participants']);
+ if (isset($args['num_participants'])) {
+ unset($defaults['num_participants']);
+ }
+ if (isset($args['num_responses'])) {
+ unset($defaults['num_responses']);
+ }
+ $args = array_merge($args, $length_defaults[$args['length']]);
+ }
+ } else {
+ if (!$args['num_participants']) {
+ $args['num_participants'] = 2;
+ }
+ if (!$args['num_responses']) {
+ $args['num_responses'] = 0;
+ }
+ }
+
+ // Fetch topics for the conversation if 'subject' is not passed
+ if (!$args['subject']) {
+ $default_topics = get_option('options_default_topic_pool') ? rl_get_textarea_meta_as_array("option", "default_topic_pool") : [];
+ $campaign_topics = get_post_meta($campaign_id, 'campaign_conversation_topics', true) ? rl_get_textarea_meta_as_array($campaign_id, 'campaign_conversation_topics') : [];
+ $all_topics = array_merge($default_topics, $campaign_topics);
+ // $all_topics_count = count($all_topics);
+ // log_to_file("generate_conversation - All topics $all_topics_count: ", $all_topics);
+ if (!empty($all_topics)) {
+ $args['subject'] = $all_topics[array_rand($all_topics)];
+ } else {
+ $args['subject'] = __('General Inquiry', 'rl-mailwarmer');
+ }
+ }
+
+ // Fetch campaign duration
+ $start_date = get_field('start_date', $campaign_id);
+ $warmup_period = get_field('warmup_period', $campaign_id);
+ $end_date = date('Ymd', strtotime("+{$warmup_period} weeks", strtotime($start_date)));
+
+ // Fetch scrubber pool if 'received_by' is not passed
+ if (!$args['received_by']) {
+ $args['received_by'] = self::get_email_accounts_for_pool($campaign_id, 'scrubber_test');
+ // $args['received_by'] = get_field('scrubber_pool', 'option') ? rl_get_textarea_meta_as_array('option', 'options_scrubber_pool') : '';
+ }
+
+ // Fetch the CC Pool
+ $args['cc_pool'] = self::get_email_accounts_for_pool($campaign_id, 'cc');
+
+ // Fetch the Reply pool if there will be any replies needed
+ if ( intval($args['num_responses']) > 0) {
+ $args['reply_pool'] = self::get_email_accounts_for_pool($campaign_id, 'reply');
+ }
+
+ // Fetch campaign target profession
+ $target_profession = get_field('target_profession', $campaign_id);
+
+ // Generate the prompt
+ $prompt = sprintf(
+ "Generate a JSON email conversation with distinct participant personalities, up to 5%% errors, and replies only from the sender or addresses in both 'can_reply' and 'to_pool'. Include: from, to, cc, subject, body\n%s",
+ json_encode([
+ 'profession' => $target_profession,
+ 'from' => $args['initiated_by'],
+ 'to_pool' => $args['received_by'],
+ 'subject' => $args['subject'],
+ 'num_of_replies' => $args['num_responses'],
+ 'num_of_participants' => $args['num_participants'],
+ 'can_reply' => $args['reply_pool'],
+ 'available_to_cc' => $args['cc_pool'],
+ ], JSON_PRETTY_PRINT)
+ );
+
+ log_to_file("generate_conversation - Prompt: $prompt");
+
+ // Save the conversation to the database
+ $conversation_steps = []; // Placeholder until AI generates steps
+ $conversation_id = RL_MailWarmer_DB_Helper::insert_conversation($campaign_id, $conversation_steps, $prompt, $chat_model);
+
+ if (!$conversation_id) {
+ return new WP_Error('db_error', __('Failed to create conversation.', 'rl-mailwarmer'));
+ }
+
+ // Generate AI content for the conversation
+ $ai_response[] = self::clean_ai_response(self::generate_ai_conversation($prompt,$chat_model));
+ // $ai_response = self::clean_ai_response(get_post_meta($campaign_id, 'last_ai_response', true));
+
+ if (is_wp_error($ai_response[0])) {
+ return $ai_response[0]; // Handle AI generation failure gracefully
+ }
+
+
+ // Update the conversation with generated steps - ??????????????????
+ // self::update_conversation_steps($conversation_id, $ai_response);
+
+ log_to_file("generate_conversation - AI response: ", $ai_response);
+ // update_post_meta($campaign_id, 'last_ai_response', wp_slash($ai_response));
+
+ // Parse the AI response and save messages
+ // $parsed_steps = self::parse_ai_response($ai_response);
+ $array_args = ['step' => ''];
+ $parsed_steps = array_fill(0, $args['num_responses'], $array_args );
+
+ // Save each timestamp into the parsed steps
+ $parsed_steps = self::add_timestamps_to_conversation($parsed_steps, $start_date);
+
+ log_to_file("generate_conversation - parsed_steps: ", $parsed_steps);
+ foreach ($parsed_steps as $index => $step) {
+ if (!empty($step['scheduled_for'])) {
+ $is_first_message = ($index === 0);
+ if (is_array($step['to'])) {
+ $step['to'] = implode(', ', $step['to']);
+ }
+ if (is_array($step['cc'])) {
+ $step['cc'] = implode(', ', $step['cc']);
+ }
+ // log_to_file("generate_conversation - message body: ", $step['body']);
+
+ // RL_MailWarmer_DB_Helper::insert_message(
+ // $campaign_id,
+ // $conversation_id,
+ // $step['scheduled_for'],
+ // $step['from'],
+ // $step['to'],
+ // $step['cc'],
+ // $step['subject'],
+ // $step['body'],
+ // $is_first_message
+ // );
+ }
+ }
+
+
+ return $conversation_id;
+ }
+
+
+ /**
+ * Fetch all published email accounts matching the campaign domain with "include_in_cc_pool" set to true.
+ *
+ * @param int $campaign_id The ID of the campaign.
+ * @param string $pool The name of the pool. Valid options are: cc, reply
+ * @return array The email addresses to include in the CC pool.
+ */
+ private static function get_email_accounts_for_pool($campaign_id, $pool) {
+ // Get the domain associated with the campaign
+ $domain_id = get_post_meta($campaign_id, 'domain', true);
+
+ if (empty($domain_id)) {
+ return []; // Return an empty array if no domain is found
+ }
+
+ // Query email accounts
+ $args = [
+ 'post_type' => 'email-account',
+ 'post_status' => 'publish',
+ 'meta_query' => [
+ 'relation' => 'AND',
+ [
+ 'key' => 'include_in_' . $pool . '_pool',
+ 'value' => true,
+ 'compare' => '=',
+ ],
+ // [
+ // 'key' => 'domain',
+ // 'value' => $domain_id,
+ // 'compare' => '=',
+ // ],
+ ],
+ 'fields' => 'ids', // Retrieve only IDs for efficiency
+ 'posts_per_page' => -1, // Fetch all matching posts
+ ];
+
+ $query = new WP_Query($args);
+
+ $email_pool = [];
+
+ if ($query->have_posts()) {
+ foreach ($query->posts as $post_id) {
+ $email_address = get_the_title($post_id);
+ if (!empty($email_address)) {
+ $email_pool[] = $email_address;
+ }
+ }
+ }
+
+ return $email_pool;
+ }
+
+ /**
+ * Add timestamps to parsed conversation steps.
+ *
+ * @param array $parsed_steps The parsed steps of the conversation.
+ * @param string $start_date The starting date for the timestamps.
+ * @return array The updated conversation steps with timestamps and step indices.
+ */
+ private static function add_timestamps_to_conversation($parsed_steps, $start_date, $end_date = NULL, $filled_dates = []) {
+ // Ensure parsed_steps is an array
+ if (!is_array($parsed_steps) || empty($parsed_steps)) {
+ throw new Exception(__('Parsed steps must be a non-empty array.', 'rl-mailwarmer'));
+ }
+
+ // Initialize the updated steps array
+ $updated_steps = [];
+
+ // Iterate through each step and assign timestamps
+ foreach ($parsed_steps as $index => $step) {
+
+ // If this is not the first step, ensure the start_date is at least 5 minutes after the previous step
+ if ($index > 0) {
+ $previous_timestamp = strtotime($updated_steps[$index - 1]['scheduled_for']);
+ $start_date = date('Y-m-d H:i:s', max($timestamp, $previous_timestamp + 300)); // 300 seconds = 5 minutes
+ }
+
+ // Use the helper function to generate a random timestamp
+ $timestamp = self::generate_random_date_time($start_date, $end_date, $filled_dates );
+ // log_to_file("add_timestamps_to_conversation - Returned random timestamp: $timestamp");
+
+ // Update the step with the scheduled timestamp and step index
+ $step['scheduled_for'] = date('Y-m-d H:i:s', $timestamp);
+ $step['step'] = $index + 1;
+
+ // Update the starting date for the next iteration
+ $start_date = date('Y-m-d H:i:s', $timestamp);
+
+ // Append the updated step
+ $updated_steps[] = $step;
+ }
+ // log_to_file("add_timestamps_to_conversation - Updated steps: ", $updated_steps);
+
+ return $updated_steps;
+ }
+
+ /**
+ * Generate a semi-randomized timestamp with a random date and time.
+ *
+ * @param string $start_date The start date in 'Y-m-d' format. Defaults to today.
+ * @param string $end_date The end date in 'Y-m-d' format. Defaults to today + 7 days.
+ * @param string $start_time The start time in 'H:i' format (e.g., '08:00'). Defaults to '08:00'.
+ * @param string $end_time The end time in 'H:i' format (e.g., '18:00'). Defaults to '18:00'.
+ * @return int The random timestamp.
+ */
+ private static function generate_random_date_time($start_date = null, $end_date = null, $filled_dates = [], $start_time = '07:20', $end_time = '21:00' ) {
+ // Default dates
+ $start_date = $start_date ?: date('Y-m-d H:i');
+ $end_date = $end_date ?: date('Y-m-d H:i', strtotime($start_date . ' +8 days'));
+
+ log_to_file("generate_random_date_time - Generating random date time between $start_date & $end_date");
+
+ $start_date_timestamp = strtotime($start_date);
+ $end_date_timestamp = strtotime($end_date);
+ $retry = false;
+
+ if ($end_date_timestamp < $start_date_timestamp) {
+
+ throw new Exception('generate_random_date_time - End date must be greater than or equal to start date.');
+
+ } else if ( $end_date_timestamp === $start_date_timestamp ) {
+
+ // Single-part conversations have the same start & end date
+
+ $random_date = date('Y-m-d', $start_date_timestamp);
+ // log_to_file("generate_random_date_time - Using the same start & end date");
+
+ } else {
+
+ // Generate a random date within the timeframe
+ log_to_file("generate_random_date_time - Finding a date that's not in \$filled_dates");
+ do {
+
+ if ($retry) {
+ // log_to_file("generate_random_date_time - Regenerating random date because it was found in filled_dates.");
+
+ }
+
+ // Generate a random date between the start and end dates
+ $random_date_timestamp = mt_rand($start_date_timestamp, $end_date_timestamp);
+ $random_date = date('Y-m-d', $random_date_timestamp);
+
+ $retry = true;
+ log_to_file("generate_random_date_time - Random date $random_date");
+ log_to_file("generate_random_date_time - Filled Dates: ", $filled_dates);
+ } while (in_array($random_date, $filled_dates));
+ log_to_file("generate_random_date_time - Random date found!");
+ }
+
+
+
+
+ // Generate a random time within the specified range for the selected date
+ log_to_file("generate_random_date_time - calling generate_random_time( $random_date, $start_time, $end_time)");
+ $random_time_timestamp = self::generate_random_time($random_date, $start_time, $end_time);
+ // log_to_file("generate_random_date_time - Returned random timestamp: $random_time_timestamp");
+
+ return $random_time_timestamp;
+ }
+
+ /**
+ * Generate a random timestamp for a given date between specific hours.
+ *
+ * @param string $date The date in 'Y-m-d' format.
+ * @param string $start_time The start time in 'H:i' format.
+ * @param string $end_time The end time in 'H:i' format.
+ * @return int The random timestamp.
+ */
+ private static function generate_random_time($date, $start_time, $end_time) {
+ // Convert start and end times to timestamps
+ $start_timestamp = strtotime("$date $start_time");
+ $end_timestamp = strtotime("$date $end_time");
+ // log_to_file("generate_random_time - Start: $start_timestamp End: $end_timestamp");
+
+ if ($end_timestamp < $start_timestamp) {
+ throw new Exception('generate_random_time - End time must be greater or equal to start time.');
+ }
+
+ // Generate a random timestamp within the time range
+ $timestamp = mt_rand($start_timestamp, $end_timestamp);
+ // log_to_file("generate_random_time - Random Timestamp: $timestamp");
+ return $timestamp;
+ }
+
+ /**
+ * Generate AI content for the conversation using OpenAI's ChatGPT API.
+ *
+ * @param string $prompt The prompt to send to ChatGPT.
+ * @return string|WP_Error The AI response or WP_Error on failure.
+ */
+ public static function generate_ai_conversation($prompt, $model) {
+ // $api_key = getenv('OPENAI_API_KEY');
+ $api_key = get_field('chatgpt_api_key', 'option');
+
+ if (empty($api_key)) {
+ return new WP_Error('missing_api_key', __('OpenAI API key is not configured.', 'rl-mailwarmer'));
+ }
+
+ try {
+ $client = \OpenAI::client($api_key);
+ $response = $client->chat()->create([
+ 'model' => $model,
+ 'messages' => [
+ ['role' => 'system', 'content' => 'You are a helpful assistant specialized in generating email conversations.'],
+ ['role' => 'user', 'content' => $prompt],
+ ],
+ 'temperature' => 0.7,
+ 'max_tokens' => 3000,
+ ]);
+
+ return self::clean_ai_response($response['choices'][0]['message']['content']);
+ } catch (\Exception $e) {
+ return new WP_Error('api_error', $e->getMessage());
+ }
+ }
+
+ /**
+ * Clean the AI response by removing Markdown-style code block delimiters.
+ *
+ * @param string $response The raw AI response.
+ * @return string The cleaned JSON string.
+ */
+ private static function clean_ai_response($response) {
+ // Use regex to remove the ```json and ``` delimiters
+ return preg_replace('/^```json\s*|```$/m', '', trim($response));
+ }
+
+ private static function normalize_conversation_format($ai_response_string) {
+ log_to_file("normalize_conversation_format - Running");
+ // log_to_file("normalize_conversation_format - Response: {}");
+
+ // Decode JSON string to PHP array/object
+ $decoded = json_decode($ai_response_string, true);
+
+ // If decoding failed, return empty array
+ if (!$decoded) {
+ log_to_file("normalize_conversation_format - Failed to json_decode ai_response_string");
+ return [];
+ }
+ // Check if the response is a flat object (single email)
+ if (isset($decoded['from'])) {
+ // It's a single email, wrap it in an array
+ log_to_file("normalize_conversation_format - Single response. Converting to multi-dimensional array");
+
+ return [$decoded];
+ }
+
+ // Already a multi-dimensional array, return as is
+ log_to_file("normalize_conversation_format - Returning as is");
+
+ return $decoded;
}
}
@@ -429,4 +1197,109 @@ function rl_mailwarmer_process_upcoming_conversations_handler() {
} catch (Exception $e) {
wp_send_json_error(__('Error processing conversations: ', 'rl-mailwarmer') . $e->getMessage());
}
-}
\ No newline at end of file
+}
+
+
+/**
+ * Add a metabox to the WP dashboard for monitoring and processing the conversation queue.
+ */
+add_action('wp_dashboard_setup', function () {
+ wp_add_dashboard_widget(
+ 'rl_mailwarmer_conversation_queue',
+ __('Conversation Queue', 'rl-mailwarmer'),
+ 'rl_mailwarmer_render_conversation_queue_widget'
+ );
+});
+
+/**
+ * Render the Message Queue dashboard widget.
+ */
+function rl_mailwarmer_render_conversation_queue_widget() {
+ global $wpdb;
+
+ // Count past-due conversations
+ $table_name = $wpdb->prefix . 'rl_mailwarmer_conversations';
+ $start_time = date('Y-m-d H:i:s', strtotime('-96 hours'));
+ $end_time = date('Y-m-d H:i:s', strtotime('+12 hours'));
+ $end_of_today = date('Y-m-d 11:59:59', current_time('timestamp'));
+ $past_due_count = $wpdb->get_var(
+ $wpdb->prepare(
+ "SELECT COUNT(*) FROM $table_name WHERE status = %s AND first_message_timestamp BETWEEN %s AND %s",
+ 'new',
+ $start_time,
+ $end_time
+ )
+ );
+ // $conversations = $wpdb->get_results(
+ // $wpdb->prepare(
+ // "SELECT * FROM $conversation_table WHERE status = %s AND first_message_timestamp BETWEEN %s AND %s ORDER BY first_message_timestamp ASC LIMIT 5",
+ // 'new',
+ // $start_time,
+ // $end_time
+ // ),
+ // ARRAY_A
+ // );
+
+ ?>
+
+
+
+ __('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 conversations
+ // log_to_file("wp_ajax_rl_mailwarmer_process_conversation_queue - Trying process_pending_conversations()");
+ $processed_count = RL_MailWarmer_Conversation_Handler::process_upcoming_conversations();
+
+ wp_send_json_success(['processed_count' => $processed_count]);
+ } catch (Exception $e) {
+ wp_send_json_error(['message' => $e->getMessage()]);
+ }
+});
diff --git a/includes/class-rl-mailwarmer-db-helper.php b/includes/class-rl-mailwarmer-db-helper.php
index 732b257..9262f80 100644
--- a/includes/class-rl-mailwarmer-db-helper.php
+++ b/includes/class-rl-mailwarmer-db-helper.php
@@ -19,15 +19,18 @@ class RL_MailWarmer_DB_Helper {
$conversation_sql = "CREATE TABLE IF NOT EXISTS `{$wpdb->prefix}" . self::$conversations_table . "` (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
campaign_id BIGINT(20) UNSIGNED NOT NULL,
+ email_account_id BIGINT(20) UNSIGNED NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
status VARCHAR(20) DEFAULT 'new' NOT NULL,
first_message_timestamp DATETIME DEFAULT NULL,
prompt LONGTEXT DEFAULT NULL,
conversation_steps LONGTEXT DEFAULT NULL,
+ ai_response LONGTEXT DEFAULT NULL,
PRIMARY KEY (id),
KEY campaign_id_idx (campaign_id),
KEY status_idx (status),
- KEY first_message_timestamp_idx (first_message_timestamp)
+ KEY first_message_timestamp_idx (first_message_timestamp),
+ INDEX email_account_id_idx (email_account_id)
) $charset_collate;";
// Message table
@@ -35,6 +38,7 @@ class RL_MailWarmer_DB_Helper {
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
campaign_id BIGINT(20) UNSIGNED NOT NULL,
conversation_id BIGINT(20) UNSIGNED NOT NULL,
+ email_account_id BIGINT(20) UNSIGNED NOT NULL,
scheduled_for_timestamp DATETIME NOT NULL,
status ENUM('pending', 'in_progress', 'sent', 'failed') NOT NULL DEFAULT 'pending',
from_email VARCHAR(255) NOT NULL,
@@ -45,7 +49,8 @@ class RL_MailWarmer_DB_Helper {
PRIMARY KEY (id),
INDEX scheduled_idx (scheduled_for_timestamp, status),
INDEX conversation_id_idx (conversation_id),
- INDEX campaign_id_idx (campaign_id)
+ INDEX campaign_id_idx (campaign_id),
+ INDEX email_account_id_idx (email_account_id)
) $charset_collate;";
// Backup table
@@ -85,21 +90,37 @@ class RL_MailWarmer_DB_Helper {
*/
public static function insert_conversation($conversation_data) {
global $wpdb;
-
- // $wpdb->insert(
- // "{$wpdb->prefix}" . self::$conversations_table,
- // [
- // 'campaign_id' => $campaign_id,
- // 'conversation_steps' => json_encode($conversation_steps),
- // 'prompt' => $prompt,
- // ],
- // ['%d', '%s', '%s']
- // );
$wpdb->insert("{$wpdb->prefix}" . self::$conversations_table, $conversation_data);
return $wpdb->insert_id;
}
+ /**
+ * Update a conversation record.
+ *
+ * @param int $conversation_id The ID of the conversation to update.
+ * @param array $update_data An associative array of columns and values to update.
+ *
+ * @return int|false The number of rows updated, or false on error.
+ */
+ public static function update_conversation($conversation_id, $update_data) {
+ global $wpdb;
+
+ // Ensure that $conversation_id is a valid integer
+ if (!is_int($conversation_id) || $conversation_id <= 0) {
+ return false;
+ }
+
+ // Update the table with the provided data
+ $updated = $wpdb->update(
+ "{$wpdb->prefix}" . self::$conversations_table,
+ $update_data,
+ ['id' => $conversation_id] // WHERE clause
+ );
+
+ return $updated !== false ? $updated : false;
+ }
+
/**
* Insert a message record.
*/
@@ -129,6 +150,99 @@ class RL_MailWarmer_DB_Helper {
return $wpdb->insert_id;
}
+ /**
+ * Delete all conversations and messages for a given campaign ID.
+ *
+ * @param int $campaign_id The ID of the campaign.
+ */
+ public static function delete_all_conversations_messages($campaign_id) {
+ global $wpdb;
+
+ // Ensure campaign_id is an integer
+ $campaign_id = (int) $campaign_id;
+
+ $conversations_table = $wpdb->prefix . self::$conversations_table;
+ $messages_table = $wpdb->prefix . self::$messages_table;
+
+ // Delete messages
+ $delete_messages_result = $wpdb->query(
+ $wpdb->prepare(
+ "DELETE FROM $messages_table WHERE campaign_id = %d",
+ $campaign_id
+ )
+ );
+ log_to_file("delete_all_conversations_messages - delete_messages_result: ", $delete_messages_result);
+
+ // Delete conversations
+ $delete_conversations_result = $wpdb->query(
+ $wpdb->prepare(
+ "DELETE FROM $conversations_table WHERE campaign_id = %d",
+ $campaign_id
+ )
+ );
+ log_to_file("delete_all_conversations_messages - delete_conversations_result: ", $delete_conversations_result);
+ }
+
+ /**
+ * Delete all future conversations and messages for a given campaign ID.
+ *
+ * @param int $campaign_id The ID of the campaign.
+ */
+ public static function delete_future_conversations_messages($campaign_id) {
+ global $wpdb;
+
+ $conversations_table = $wpdb->prefix . self::$conversations_table;
+ $messages_table = $wpdb->prefix . self::$messages_table;
+ $current_time = current_time('mysql');
+
+ // Delete future messages
+ $wpdb->query(
+ $wpdb->prepare(
+ "DELETE FROM $messages_table WHERE campaign_id = %d AND scheduled_for_timestamp > %s",
+ $campaign_id,
+ $current_time
+ )
+ );
+
+ // Delete future conversations
+ $wpdb->query(
+ $wpdb->prepare(
+ "DELETE FROM $conversations_table WHERE campaign_id = %d AND first_message_timestamp > %s",
+ $campaign_id,
+ $current_time
+ )
+ );
+ }
+
+ /**
+ * Delete all conversations and messages older than X days.
+ *
+ * @param int $days The number of days.
+ */
+ public static function delete_old_conversations_messages($days) {
+ global $wpdb;
+
+ $conversations_table = $wpdb->prefix . self::$conversations_table;
+ $messages_table = $wpdb->prefix . self::$messages_table;
+ $threshold_date = date('Y-m-d H:i:s', strtotime("-$days days"));
+
+ // Delete old messages
+ $wpdb->query(
+ $wpdb->prepare(
+ "DELETE FROM $messages_table WHERE scheduled_for_timestamp < %s",
+ $threshold_date
+ )
+ );
+
+ // Delete old conversations
+ $wpdb->query(
+ $wpdb->prepare(
+ "DELETE FROM $conversations_table WHERE first_message_timestamp < %s",
+ $threshold_date
+ )
+ );
+ }
+
/**
* Fetch pending messages.
diff --git a/includes/class-rl-mailwarmer-domain-helper.php b/includes/class-rl-mailwarmer-domain-helper.php
index 598c359..7bd316a 100644
--- a/includes/class-rl-mailwarmer-domain-helper.php
+++ b/includes/class-rl-mailwarmer-domain-helper.php
@@ -115,20 +115,30 @@ class RL_MailWarmer_Domain_Helper {
}
$domain_name = $domain_post->post_title;
- $credentials = self::get_cloudflare_credentials($domain_post);
+ try {
+ $credentials = self::get_cloudflare_credentials($domain_post);
+
+ } catch (Exception $e) {
+ throw new Exception(__('get_cloudflare_client - Failed to find CloudFlare zone: ', 'rl-mailwarmer') . $e->getMessage());
+ return false;
+ }
-
- $client = new \GuzzleHttp\Client([
- 'base_uri' => 'https://api.cloudflare.com/client/v4/',
- 'domain' => $domain_name,
- 'api_email' => $credentials['api_email'],
- 'api_key' => $credentials['api_key'],
- 'zone_id' => $credentials['zone_id'],
- 'headers' => [
- 'Content-Type' => 'application/json',
- ],
- ]);
- return $client;
+ if ($credentials) {
+ $client = new \GuzzleHttp\Client([
+ 'base_uri' => 'https://api.cloudflare.com/client/v4/',
+ 'domain' => $domain_name,
+ 'api_email' => $credentials['api_email'],
+ 'api_key' => $credentials['api_key'],
+ 'zone_id' => $credentials['zone_id'],
+ 'headers' => [
+ 'Content-Type' => 'application/json',
+ ],
+ ]);
+ return $client;
+ } else {
+ log_to_file("get_cloudflare_client - Unable to get cloudflare client for $domain_post->post_title");
+ return false;
+ }
}
@@ -326,18 +336,36 @@ class RL_MailWarmer_Domain_Helper {
$client = self::get_cloudflare_client($domain);
// $zone_id = self::get_cloudflare_zone_id($client, $credentials, $domain_post->post_title);
- $dns_records = self::fetch_dns_records($client);
- // log_to_file("generate_domain_report - All Records: ", $dns_records);
- $report = [
- 'domain_health' => self::check_domain_registration($domain_name),
- 'a_record' => self::check_a_record($dns_records),
- 'mx_record' => self::check_mx_record($dns_records),
- 'spf_record' => self::check_spf_record($dns_records),
- 'dkim_records' => self::check_dkim_record($dns_records),
- 'dmarc_record' => self::check_dmarc_record($dns_records),
- 'blacklists' => self::check_blacklists($domain_name),
- ];
+ if ($client) {
+ $dns_records = self::fetch_dns_records($client);
+ log_to_file("generate_domain_report - All Records: ", $dns_records);
+
+
+
+ if (isset($dns_records[0])) {
+ $report = [
+ 'domain_health' => self::check_domain_registration($domain_name),
+ 'a_record' => self::check_a_record($dns_records),
+ 'mx_record' => self::check_mx_record($dns_records),
+ 'spf_record' => self::check_spf_record($dns_records),
+ 'dkim_records' => self::check_dkim_record($dns_records),
+ 'dmarc_record' => self::check_dmarc_record($dns_records),
+ 'blacklists' => self::check_blacklists($domain_name),
+ ];
+ } else {
+ $report = [
+ 'domain_health' => self::check_domain_registration($domain_name),
+ 'blacklists' => self::check_blacklists($domain_name),
+ ];
+ }
+ } else {
+ log_to_file("generate_domain_report - Unable to connect to CloudFlare for $domain_name");
+ $report = [
+ 'domain_health' => self::check_domain_registration($domain_name),
+ 'blacklists' => self::check_blacklists($domain_name),
+ ];
+ }
// log_to_file("generate_domain_report - Health Report for $domain_name: ", $report);
@@ -439,13 +467,13 @@ class RL_MailWarmer_Domain_Helper {
*/
private static function check_a_record($dns_records)
{
- $domain_name = $dns_records[0]['zone_name'];
+ $domain_name = $dns_records[0]['name'];
// log_to_file("check_a_record - Running check_mx_record for $domain_name");
foreach ($dns_records as $record) {
// Check if the record matches the criteria
// log_to_file("check_a_record - DNS Record: ", $record);
- if ( ($record['zone_name'] === $record['name']) && ($record['type'] === 'A') ) {
+ if ( ($record['name'] === $domain_name) && ($record['type'] === 'A') ) {
$ip = $record['content'];
$http_status = self::get_http_status($domain_name);
@@ -501,12 +529,12 @@ class RL_MailWarmer_Domain_Helper {
*/
private static function check_mx_record($dns_records)
{
- $domain_name = $dns_records[0]['zone_name'];
+ $domain_name = $dns_records[0]['name'];
// log_to_file("check_mx_record - Running check_mx_record for $domain_name");
foreach ($dns_records as $record) {
// Check if the record matches the criteria
- if ( ($record['zone_name'] === $domain_name) && ($record['type'] === 'MX') ) {
+ if ( ($record['name'] === $domain_name) && ($record['type'] === 'MX') ) {
$host = $record['content'];
$ptr_record = gethostbyaddr(gethostbyname($host));
@@ -546,7 +574,7 @@ class RL_MailWarmer_Domain_Helper {
log_to_file("update_mx_record - Searching for existing record");
// throw new Exception('Failed to fetch existing DNS records from CloudFlare.');
- foreach ($dns_records['result'] as $record) {
+ foreach ($dns_records as $record) {
if ($record['content'] === $content && $record['priority'] === $priority) {
$existing_record_id = $record['id'];
log_to_file("update_mx_record - Matching record found");
@@ -579,14 +607,14 @@ class RL_MailWarmer_Domain_Helper {
log_to_file("update_mx_record - Attempting to update record");
$response = self::update_dns_record($domain_post->ID, $domain_post->post_title, 'MX', $domain_post->post_title, $content, $ttl, $priority);
- $result = json_decode($response->getBody(), true);
+ // $result = json_decode($response->getBody(), true);
log_to_file("update_mx_record - Result: ", $result);
- if (!$result['success']) {
- throw new Exception('Failed to update or create the MX record in CloudFlare.');
- }
+ // if (!$result['success']) {
+ // throw new Exception('Failed to update or create the MX record in CloudFlare.');
+ // }
- return $result; // Return the CloudFlare response
+ return $response; // Return the CloudFlare response
} catch (Exception $e) {
log_to_file('update_mx_record - Error in update_mx_record: ' . $e->getMessage());
return 'Error: ' . $e->getMessage();
@@ -602,7 +630,7 @@ class RL_MailWarmer_Domain_Helper {
*/
private static function check_spf_record($dns_records)
{
- $domain_name = $dns_records[0]['zone_name'];
+ $domain_name = $dns_records[0]['name'];
// log_to_file("check_spf_record - Running check_spf_record for $domain_name");
foreach ($dns_records as $record) {
@@ -705,7 +733,7 @@ class RL_MailWarmer_Domain_Helper {
*/
private static function check_dmarc_record($dns_records)
{
- $domain_name = $dns_records[0]['zone_name'];
+ $domain_name = $dns_records[0]['name'];
// log_to_file("check_dmarc_record - Running check_dmarc_record for $domain_name");
foreach ($dns_records as $record) {
@@ -821,8 +849,7 @@ class RL_MailWarmer_Domain_Helper {
'TXT',
$name,
$wrapped_content,
- $existing_record['ttl'],
- $credentials
+ $existing_record['ttl']
);
} else {
return self::update_dns_record(
@@ -832,7 +859,6 @@ class RL_MailWarmer_Domain_Helper {
$name,
$wrapped_content,
3600, // Default TTL
- $credentials
);
}
}
@@ -848,7 +874,7 @@ class RL_MailWarmer_Domain_Helper {
*/
private static function check_dkim_record($dns_records, $selectors = [])
{
- $domain_name = $dns_records[0]['zone_name'];
+ $domain_name = $dns_records[0]['name'];
// log_to_file("check_dkim_record - Running check_dkim_record for $domain_name");
$dkim_records = [];
@@ -1072,7 +1098,28 @@ class RL_MailWarmer_Domain_Helper {
}
}
} else {
- $results['dkim'] = 'No servers selected for the domain.';
+ log_to_file("fix_deliverability_dns_issues - No servers selected for the domain. Choosing system default.");
+ $server = get_field('defaut_mailferno_mx', 'option');
+ $server_id = $server->ID;
+ $selector = get_field('dkim_selector', $server_id);
+ $value = get_field('dkim_value', $server_id);
+ if ($selector && $value) {
+ $dkim_record = $findRecord($dns_records, "{$selector}._domainkey", true);
+ if (!$dkim_record) {
+ try {
+ $dkim_result = self::update_dkim_record($domain_post, $selector, 'add', $value);
+ $results['dkim'][$selector] = $dkim_result
+ ? "DKIM record for selector '{$selector}' added successfully."
+ : "Failed to add DKIM record for selector '{$selector}'.";
+ } catch (Exception $e) {
+ $results['dkim'][$selector] = 'Error: ' . $e->getMessage();
+ }
+ } else {
+ $results['dkim'][$selector] = "DKIM record for selector '{$selector}' already exists.";
+ }
+ } else {
+ $results['dkim'][$server_id] = 'Missing DKIM selector or value for server: ' . $server_id;
+ }
}
// DMARC
diff --git a/includes/class-rl-mailwarmer-email-account-helper.php b/includes/class-rl-mailwarmer-email-account-helper.php
index e317acf..a0ffad9 100644
--- a/includes/class-rl-mailwarmer-email-account-helper.php
+++ b/includes/class-rl-mailwarmer-email-account-helper.php
@@ -149,23 +149,25 @@ class RL_MailWarmer_Email_Helper
*/
public static function check_mail_login($email_account, $protocol = null)
{
- log_to_file("check_mail_login - Email account id: {$email_account}");
+ // 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') {
+ // log_to_file("check_mail_login - Not an email account post-type");
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}");
- // log_to_file("check_mail_login - Email Provider ID $email_provider_id");
+ // 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);
+ // log_to_file("check_mail_login - Email Provider Defaults: ", $defaults);
// Fetch saved settings
$saved_settings = [
+ 'email_address' => $post->post_title,
'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),
@@ -180,22 +182,22 @@ class RL_MailWarmer_Email_Helper
// Merge saved settings with defaults
$settings = array_merge($defaults, array_filter($saved_settings));
- // log_to_file("check_mail_login - Using settings: ", $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);
+ $imap_result = self::validate_imap_connection($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);
+ $smtp_result = self::validate_smtp_connection($settings, true);
+ // log_to_file("check_mail_login - SMTP Result for " . $post->post_title . ": ", $smtp_result);
$results['SMTP'] = $smtp_result ? __('SUCCESS', 'rl-mailwarmer') : $smtp_result->get_error_message();
update_post_meta($post->ID, 'smtp_status', $results['SMTP']);
}
@@ -223,34 +225,40 @@ class RL_MailWarmer_Email_Helper
/**
* 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).
+ * @param array $settings The server settings (email_account, imap_server, imap_port, imap_password).
* @return bool True if the connection is successful, false otherwise.
*/
- private static function validate_imap_connection($email, $settings)
+ public static function validate_imap_connection($settings)
{
- if ( empty($settings['imap_server']) || empty($settings['imap_port']) ) {
+ if ( empty($settings['email_address']) || empty($settings['mail_password']) || empty($settings['imap_server']) || empty($settings['imap_port']) ) {
+ // log_to_file("validate_imap_connection - Incomplete connection information");
return false; // Missing required settings
}
- if (!empty($settings['imap_password'])) {
- $password = $settings['imap_password'];
- } else {
- $password = $settings['mail_password'];
- }
+ $password = $settings['mail_password'];
+ $email = $settings['email_address'];
+
+ // log_to_file("validate_imap_connection - Checking IMAP connection for {$email} using: ", $settings);
+ $imap_server = '{' . $settings['imap_server'] . ':' . $settings['imap_port'] . '/imap/ssl}';
+
+ // log_to_file("validate_imap_connection - Trying to open stream for {$email} : {$password} @ {$imap_server}");
// Try connecting to the IMAP server
$imap_stream = @imap_open(
- '{' . $settings['imap_server'] . ':' . $settings['imap_port'] . '/imap/ssl}',
+ $imap_server,
$email,
$password
);
if ($imap_stream) {
+ // log_to_file("validate_imap_connection - Stream opened. Closing!");
imap_close($imap_stream); // Close connection if successful
return true;
}
+
+ // log_to_file("validate_imap_connection - Unable to open stream");
+
return false; // Connection failed
}
@@ -262,21 +270,21 @@ class RL_MailWarmer_Email_Helper
* @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)
+ public static function validate_smtp_connection($settings, $send_test_email = false)
{
- if (empty($settings['smtp_server']) || empty($settings['smtp_port']) ) {
+
+ if ( empty($settings['email_address']) || empty($settings['mail_password']) || empty($settings['smtp_server']) || empty($settings['smtp_port']) ) {
+ // log_to_file("validate_smtp_connection - Incomplete connection information");
return false; // Missing required settings
}
+ $email = $settings['email_address'];
+ $password = $settings['mail_password'];
+ // log_to_file("validate_smtp_connection - Settings for {$email}: ", $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}";
+ $email_body = "This is a test email to verify SMTP connection for {$email}\n\n{$signature}
MFTID-0000000000000000";
try {
// Create the SMTP transport
@@ -298,8 +306,10 @@ class RL_MailWarmer_Email_Helper
->to($test_to_email)
->subject('SMTP Connection Test for ' . $email)
->html($email_body);
-
- $mailer->send($test_email);
+
+ if ($send_test_email) {
+ $mailer->send($test_email);
+ }
return true;
} catch (Exception $e) {
@@ -374,7 +384,7 @@ class RL_MailWarmer_Email_Helper
'email_provider' => $mailferno_default_email_provider,
],
]);
- log_to_file("generate_random_accounts - Added email account to local server: $post_id");
+ // log_to_file("generate_random_accounts - Added email account to local server: $post_id");
if ($post_id && !is_wp_error($post_id)) {
$generated_accounts[] = [
@@ -383,16 +393,16 @@ class RL_MailWarmer_Email_Helper
'password' => $random_password,
'post_id' => $post_id,
];
- log_to_file("generate_random_accounts - {$first_name} {$last_name}\t{$email_address}\t{$random_password}");
+ // 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);
+ // 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']);
+ // 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);
+ // 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);
+ // 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));
@@ -537,7 +547,7 @@ class RL_MailWarmer_Email_Helper
} else {
return new WP_Error('invalid_action', __('Invalid action specified.', 'rl-mailwarmer'));
}
- log_to_file("modify_email_account_on_server - SSH Command: ", $command);
+ // log_to_file("modify_email_account_on_server - SSH Command: ", $command);
// Execute the command via SSH
// $ssh = new phpseclib\Net\SSH2($server_ip);
@@ -550,7 +560,7 @@ class RL_MailWarmer_Email_Helper
} else {
// Fallback to password-based authentication
// $key = $server_password;
- log_to_file("modify_email_account_on_server - Server $$server_id ssh_private_key empty");
+ // 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'));
}
diff --git a/includes/class-rl-mailwarmer-message-handler.php b/includes/class-rl-mailwarmer-message-handler.php
index 8b490dd..4caf5ce 100644
--- a/includes/class-rl-mailwarmer-message-handler.php
+++ b/includes/class-rl-mailwarmer-message-handler.php
@@ -5,10 +5,13 @@
*/
use Symfony\Component\Mime\Email;
+use Symfony\Component\Mime\Address;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport\Transport;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use PhpImap\Mailbox;
+use phpseclib3\Net\SSH2;
+use phpseclib3\Crypt\PublicKeyLoader;
if (!defined('ABSPATH')) {
@@ -23,47 +26,176 @@ class RL_MailWarmer_Message_Handler {
public static function process_pending_messages() {
// log_to_file("process_pending_messages - Running");
global $wpdb;
+ $results = [
+ 'success' => 0,
+ 'failure' => 0,
+ ];
// 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',
+ "SELECT * FROM $table_name WHERE status = %s AND scheduled_for_timestamp < %s ORDER BY scheduled_for_timestamp ASC LIMIT 10",
+ 'scheduled',
current_time('mysql')
),
ARRAY_A
);
- if (empty($messages)) {
- // log_to_file("process_pending_messages - messages empty");
- return;
- }
+ // 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");
+ $messages_count = count($messages);
+ if ($messages_count > 0) {
+ action_log("process_pending_messages - Processing {$messages_count} messages" );
+
+ 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);
+ // }
+ log_to_file("process_pending_messages - trying send_message for {$message['id']} from {$message['from_email']} to ", $message['to_email']);
$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 {
+ // Update message status to 'sent' on success
+ if ($result) {
+ self::update_message_status($message['id'], 'sent');
+ $results['success']++;
+ log_to_file("process_pending_messages - Success sending message: {$message['id']}");
+ action_log("process_pending_messages - Sent email {$message['id']} from {$message['from_email']} to ", $message['to_email']);
+ } else {
+ self::update_message_status($message['id'], 'failed');
+ log_to_file("process_pending_messages - Error sending message: {$message['id']}");
+ $results['failure']++;
+ }
+ } catch (Exception $e) {
+ // Handle errors gracefully and log them
+ log_to_file('process_pending_messages - Error processing message ID ' . $message['id'] . ': ' . $e->getMessage());
self::update_message_status($message['id'], 'failed');
+ $results['failure']++;
}
- } 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);
}
- sleep(3);
+ log_to_file("process_pending_messages - Results: ", $results);
+ action_log("process_pending_messages - Finished processing {$messages_count} messages with {$results['success']} sent and {$results['failure']} failures");
}
+
+ return $results;
+ }
+
+ /**
+ * 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'];
+ if (!empty($connection_info['smtp_password'])) {
+ $password = $connection_info['smtp_password'];
+ } else {
+ $password = $connection_info['mail_password'];
+ }
+
+ // Check required fields
+ if (empty($email_data['to']) || empty($email_data['from']) || empty($email_data['subject']) || empty($email_data['text_body'])) {
+
+ // log_to_file("send_message - Missing required fields for sending the email");
+ throw new Exception(__('Missing required fields for sending the email.', 'rl-mailwarmer'));
+ }
+
+ // Create the SMTP transport
+ try {
+ // log_to_file("send_message - Creating Transport");
+ // 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($email_data['from']);
+ $transport->setPassword($password);
+
+ $to_addresses = $email_data['to'];
+ if (!is_array($to_addresses)) {
+ $to_addresses = json_decode($to_addresses, true);
+ }
+ $to_addresses_type = gettype($to_addresses);
+ // log_to_file("send_message - To ({$to_addresses_type}): ", $to_addresses);
+
+ // Create the mailer
+ $mailer = new Symfony\Component\Mailer\Mailer($transport);
+
+ // Send an email
+
+ $email_message = (new Symfony\Component\Mime\Email())
+ ->from(new Address($email_data['from'], $email_data['name']))
+ ->to(...$to_addresses)
+ ->subject($email_data['subject'])
+ ->text($email_data['text_body'])
+ ->html($email_data['html_body']);
+
+ // Add headers
+ $campaign_tracking_id = $email_data['campaign_tracking_id'];
+ // $previous_message_id = $message['previous_message_id'];
+ // if ($previous_message_id) {
+ // $campaign_tracking_id .= '-' . $previous_message_id;
+ // }
+ $email_message->getHeaders()->addTextHeader('X-MFTID', $campaign_tracking_id);
+ // log_to_file("send_message - Creating email with MFTID: {$campaign_tracking_id}");
+
+ // log_to_file("send_message - Trying to send email.");
+ $smtp_result = $mailer->send($email_message);
+
+ // log_to_file("send_message - Message sent!", $smtp_result);
+ return true;
+ } catch (TransportExceptionInterface $e) {
+ log_to_file("send_message - Error sending email {$message['id']}: " . $e->getMessage());
+ return false;
+ }
+ }
+
+ public static function search_email_by_x_mftid($imap_stream, $imap_server, $campaign_tracking_id) {
+ $folders = imap_list($imap_stream, $imap_server, '*');
+ $result = ['folder' => null, 'email' => null];
+
+ $search_term = '/X\-MFTID: ' . preg_quote($campaign_tracking_id, '/') . '.+/i';
+ log_to_file("search_email_by_x_mftid - search term: {$search_term}");
+
+ foreach ($folders as $folder) {
+ $decoded_folder = imap_utf7_decode($folder);
+ // log_to_file("search_email_by_x_mftid - decoded_folder: ", $decoded_folder);
+
+ $status = imap_status($imap_stream, $decoded_folder, SA_MESSAGES);
+ if ($status->messages > 0) {
+ log_to_file("search_email_by_x_mftid - Searching {$decoded_folder}");
+ $emails = imap_search($imap_stream, 'TEXT "' . $campaign_tracking_id . '"', SE_UID);
+ if ($emails) {
+ $result['folder'] = $decoded_folder;
+ $result['email'] = $emails;
+ break;
+ }
+ }
+ }
+ return $result;
}
/**
@@ -90,6 +222,7 @@ class RL_MailWarmer_Message_Handler {
// Fetch connection details
// log_to_file("prepare_email_data - Getting connection info");
+ $full_name = get_post_meta($from_post_id, 'full_name', true);
$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);
@@ -107,16 +240,27 @@ class RL_MailWarmer_Message_Handler {
// 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']);
+ // $to_emails = json_decode();
+ $to_emails = is_array($message['to_email']) ? $message['to_email'] : json_decode($message['to_email']);
+ $cc_emails = is_array($message['cc']) ? $message['cc'] : json_decode($message['cc']);
+
+ $campaign_tracking_id = $message['campaign_tracking_id'] . '-' . $message['id'];
+ $previous_message_id = $message['previous_message_id'];
+ $text_body = $message['body'] . "\n\n" . $campaign_tracking_id;
+ $html_body = $message['body'] . "
{$campaign_tracking_id}";
return [
+ 'id' => $message['id'],
'connection_info' => $connection_info,
- 'to' => array_filter(array_map('trim', $to_emails)),
- 'cc' => array_filter(array_map('trim', $cc_emails)),
+ 'to' => array_filter(array_map('trim', array_map('stripslashes', $to_emails))),
+ 'cc' => array_filter(array_map('trim', array_map('stripslashes', $cc_emails))),
'subject' => $message['subject'],
- 'body' => $message['body'],
+ 'text_body' => $text_body,
+ 'html_body' => $html_body,
'from' => $message['from_email'],
+ 'name' => $full_name,
+ 'campaign_tracking_id' => $campaign_tracking_id,
+ 'previous_message_id' => $previous_message_id,
];
}
@@ -163,351 +307,228 @@ class RL_MailWarmer_Message_Handler {
);
}
+ // /**
+ // * 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);
+
+ // // Validate required fields
+ // if (empty($email_data['to']) || empty($email_data['from']) || empty($email_data['subject']) || empty($email_data['text_body'])) {
+ // throw new Exception(__('Missing required fields for replying to the email.', 'rl-mailwarmer'));
+ // }
+
+ // // Extract connection info
+ // $connection_info = $email_data['connection_info'];
+ // $emails = '';
- /**
- * 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);
+ // try {
+ // // log_to_file("reply_message - Trying to reply via IMAP {$connection_info['imap_server']}:{$connection_info['imap_port']}");
+ // // $imap = new \PhpImap\Mailbox(
+ // // sprintf('{%s:%s/imap/ssl}', $connection_info['imap_server'], $connection_info['imap_port']),
+ // // $connection_info['username'],
+ // // $connection_info['mail_password'],
+ // // null,
+ // // 'UTF-8'
+ // // );
- // Prepare email data and connection info
- $email_data = self::prepare_email_data($message);
+ // // $imap->checkMailbox();
- // 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
+ // // // Search for the email with the matching subject
+ // // log_to_file("reply_message - Searching for message with X-MFTID");
+ // // $emails = $imap->searchMailbox('HEADER X-MFTID "' . addslashes($email_data['campaign_tracking_id']) . '"');
-// // 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'
-// );
+ // $password = $connection_info['mail_password'];
+ // $email = $connection_info['username'];
+
+ // log_to_file("reply_message - Trying to reply via IMAP for {$email} using: ", $connection_info);
-// // 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]);
+ // $imap_server = '{' . $connection_info['imap_server'] . ':' . $connection_info['imap_port'] . '/imap/ssl}';
-// // Prepare and send the reply via IMAP
-// $imap->reply(
-// $emails[0],
-// $email_data['body'],
-// ['from' => $email_data['from'], 'cc' => $email_data['cc']]
-// );
+ // log_to_file("reply_message - Trying to open stream for {$email} : {$password} @ {$imap_server}");
+ // // Try connecting to the IMAP server
+ // $imap_stream = @imap_open(
+ // $imap_server,
+ // $email,
+ // $password
+ // );
-// return true; // Reply sent successfully via IMAP
-// }
+ // if ($imap_stream) {
+ // log_to_file("reply_message - IMAP stream opened.");
+ // $imap_search_result = self::search_email_by_x_mftid($imap_stream, $imap_server, $email_data['campaign_tracking_id']);
+ // if ($imap_search_result['folder']) {
+ // log_to_file("reply_message - Email with X-MFTID found in folder: {$imap_search_result['folder']}");
+ // log_to_file("reply_message - Email: ", $imap_search_result['email']);
+ // // $emails = $imap_search_result['email'];
+ // // Continue with reply logic
+ // } else {
+ // log_to_file("No email found with X-MFTID: " . $email_data['campaign_tracking_id']);
+ // }
+ // imap_close($imap_stream);
+ // }
+ // if (!empty($emails)) {
- 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'
- );
+ // // 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);
- // 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)) {
+ // // 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']);
+ // $to_addresses = $email_data['to'];
+ // if (!is_array($to_addresses)) {
+ // $to_addresses = json_decode($to_addresses, true);
+ // }
+
+ // $mailer = new Mailer($transport);
+
+ // $reply_email = (new Email())
+ // ->from(new Address($email_data['from'], $email_data['name']))
+ // ->to(...$to_addresses)
+ // ->subject('Re: ' . $original_email->subject)
+ // ->text($email_data['text_body'])
+ // ->html($email_data['html_body']);
+
+ // // Add headers
+ // $campaign_tracking_id = $email_data['campaign_tracking_id'];
+ // $reply_email->getHeaders()->addTextHeader('X-MFTID', $campaign_tracking_id);
+
+ // // 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
+ // } else {
+ // log_to_file("reply_message - Unable to reply via IMAP. Falling back to SMTP");
+ // }
+ // } catch (Exception $e) {
+ // log_to_file('reply_message - IMAP Error: ' . $e->getMessage());
+ // }
+
+ // // Fallback to SMTP if IMAP fails
+ // try {
+ // log_to_file("reply_message - Falling back to SMTP");
+
+ // $result = self::send_message($message);
+ // return $result;
+ // // $to_addresses = $email_data['to'];
+ // // if (!is_array($to_addresses)) {
+ // // $to_addresses = json_decode($to_addresses, true);
+ // // }
+ // // log_to_file("reply_message - Creating SMTP message");
+
+ // // $smtp_reply = (new Email())
+ // // ->from(new Address($email_data['from'], $email_data['name']))
+ // // ->to(...$to_addresses)
+ // // ->subject($email_data['subject'])
+ // // ->text($email_data['text_body'])
+ // // ->html($email_data['html_body']);
+
+ // // // Add headers
+ // // $campaign_tracking_id = $email_data['campaign_tracking_id'];
+ // // $smtp_reply->getHeaders()->addTextHeader('X-MFTID', $campaign_tracking_id);
+
+ // // // Add CCs if present
+ // // // if (!empty($email_data['cc'])) {
+ // // // $smtp_reply->cc(...$email_data['cc']);
+ // // // }
+
+ // // // Create the SMTP transport
+ // // log_to_file("reply_message - Creating 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
+ // // log_to_file("reply_message - Creating SMTP mailer");
+ // // $mailer = new Symfony\Component\Mailer\Mailer($transport);
+ // // $smtp_result = $mailer->send($smtp_reply);
+ // // log_to_file("reply_message - Sent reply via fallback SMTP from {$email_data['from']}:", $smtp_result);
+ // // // log_to_file('reply_message - SMTP Send Success (?)');
+
+ // // Fallback SMTP reply sent successfully
+ // } catch (Exception $e) {
+ // log_to_file('reply_message - SMTP Error: ' . $e->getMessage());
+ // return false; // Reply failed
+ // }
+ // }
- // 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')
- )
- );
-
- ?>
-
-
-
- __('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()]);
- }
-});
diff --git a/includes/class-rl-mailwarmer-message-helper.php b/includes/class-rl-mailwarmer-message-helper.php
index a040898..a5290d9 100644
--- a/includes/class-rl-mailwarmer-message-helper.php
+++ b/includes/class-rl-mailwarmer-message-helper.php
@@ -153,3 +153,95 @@ add_action('wp_ajax_rl_delete_messages', function () {
wp_send_json_success(['message' => __('Messages deleted successfully.', 'rl-mailwarmer')]);
});
+
+
+/**
+ * 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",
+ 'scheduled',
+ current_time('mysql')
+ )
+ );
+
+ ?>
+
+
+
+ __('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()");
+ $results = RL_MailWarmer_Message_Handler::process_pending_messages();
+
+ wp_send_json_success($results);
+ } catch (Exception $e) {
+ wp_send_json_error(['message' => $e->getMessage()]);
+ }
+});
diff --git a/includes/class-rl-mailwarmer-post-tables.php b/includes/class-rl-mailwarmer-post-tables.php
index 8c688c9..dcc37fc 100644
--- a/includes/class-rl-mailwarmer-post-tables.php
+++ b/includes/class-rl-mailwarmer-post-tables.php
@@ -8,7 +8,7 @@ class PostTypeList {
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->items_per_page = isset($_GET['per_page']) ? intval($_GET['per_page']) : 25;
$this->paged = (get_query_var('paged')) ? get_query_var('paged') : 1;
// Base query args
@@ -60,22 +60,22 @@ class PostTypeList {
?>
-
+
- Edit |
+ Edit |
- get_delete_link($post_id); ?>
+ get_delete_link($post_id); ?>
- labels['no_items']); ?>
+ labels['no_items']; ?>
diff --git a/includes/class-rl-mailwarmer-scheduler.php b/includes/class-rl-mailwarmer-scheduler.php
index 59efe3e..663513d 100644
--- a/includes/class-rl-mailwarmer-scheduler.php
+++ b/includes/class-rl-mailwarmer-scheduler.php
@@ -13,8 +13,8 @@ class RL_MailWarmer_Scheduler {
add_filter('cron_schedules', [__CLASS__, 'add_cron_timings']);
// Call local functions to call the remote classes, so we can debug cron jobs
- add_action('rl_mailwarmer_process_messages', [__CLASS__, 'process_pending_messages']);
- add_action('rl_mailwarmer_process_upcoming_conversations', [__CLASS__, 'process_upcoming_conversations']);
+ add_action('rl_mailwarmer_process_messages', [__CLASS__, 'cron_process_pending_messages']);
+ add_action('rl_mailwarmer_process_upcoming_conversations', [__CLASS__, 'cron_process_upcoming_conversations']);
// add_action('rl_mailwarmer_process_messages', [RL_MailWarmer_Message_Handler::class, 'process_pending_messages']);
// add_action('rl_mailwarmer_process_upcoming_conversations', [RL_MailWarmer_Conversation_Handler::class, 'process_upcoming_conversations']);
@@ -79,16 +79,16 @@ class RL_MailWarmer_Scheduler {
/**
* Process pending messages by delegating to the Message Handler.
*/
- public static function process_pending_messages() {
- // log_to_file("schedule_cron_jobs ====================== Running Cron to process messages ========================");
- // RL_MailWarmer_Message_Handler::process_pending_messages();
+ public static function cron_process_pending_messages() {
+ action_log("====================== Running Cron to process messages ========================");
+ RL_MailWarmer_Message_Handler::process_pending_messages();
}
/**
* Process pending conversations by delegating to the Message Handler.
*/
- public static function process_upcoming_conversations() {
- // log_to_file("schedule_cron_jobs ====================== Running Cron to process conversations ========================");
- // RL_MailWarmer_Message_Handler::process_upcoming_conversations();
+ public static function cron_process_upcoming_conversations() {
+ action_log("====================== Running Cron to process conversations ========================");
+ RL_MailWarmer_Conversation_Handler::process_upcoming_conversations();
}
}
diff --git a/includes/rl-mailwarmer-functions.php b/includes/rl-mailwarmer-functions.php
index 44a0c7b..362b5e5 100644
--- a/includes/rl-mailwarmer-functions.php
+++ b/includes/rl-mailwarmer-functions.php
@@ -12,7 +12,7 @@
* Set the CUSTOM_DEBUG_LOG file in wp-config.php
*
*/
-function log_to_file($message, $data = false){
+function log_to_file($message, $data = null, $enable_backtrace = false) {
if ($message) {
$log_File = CUSTOM_DEBUG_LOG;
@@ -21,13 +21,48 @@ function log_to_file($message, $data = false){
// Convert arrays and objects to JSON format
if (is_array($data) || is_object($data)) {
- $data = json_encode($data);
+ $data = json_encode($data, JSON_PRETTY_PRINT);
$message = $message . "\r\n" . $data;
} else if ($data) {
$message = $message . " " . $data;
}
- error_log("[$date] " . $message ."\r\n",3,$log_File);
+ // Include backtrace information if enabled
+ if ($enable_backtrace) {
+ $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+ $callingFunction = $backtrace[1]['function']; // Access the calling function name
+ $backtrace_info = json_encode($backtrace, JSON_PRETTY_PRINT);
+ $message .= "\r\nBacktrace:\r\n" . $backtrace_info;
+ }
+
+ error_log("[$date] " . $message . "\r\n", 3, $log_File);
+ }
+}
+
+function action_log($message, $data = null, $enable_backtrace = false) {
+ if ($message) {
+ $log_File = ACTION_LOG;
+
+ $date = new DateTime('now', new DateTimeZone('America/Chicago'));
+ $date = $date->format("Y/m/d h:i:s");
+
+ // Convert arrays and objects to JSON format
+ if (is_array($data) || is_object($data)) {
+ $data = json_encode($data, JSON_PRETTY_PRINT);
+ $message = $message . "\r\n" . $data;
+ } else if ($data) {
+ $message = $message . " " . $data;
+ }
+
+ // Include backtrace information if enabled
+ if ($enable_backtrace) {
+ $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+ $callingFunction = $backtrace[1]['function']; // Access the calling function name
+ $backtrace_info = json_encode($backtrace, JSON_PRETTY_PRINT);
+ $message .= "\r\nBacktrace:\r\n" . $backtrace_info;
+ }
+
+ error_log("[$date] " . $message . "\r\n", 3, $log_File);
}
}
diff --git a/js/generate-timeline.js b/js/generate-timeline.js
index 40bc851..d1dae84 100644
--- a/js/generate-timeline.js
+++ b/js/generate-timeline.js
@@ -17,13 +17,14 @@ jQuery(document).ready(function ($) {
success: function (response) {
console.log(response);
if (response.success) {
- const timeline = response.data;
- let output = '
Timeline Generated:
';
- $.each(timeline, function (date, volume) {
- output += `- ${date}: ${volume} emails
`;
- });
- output += '
';
- $('#generate-timeline-result').html(output);
+ console.log(response.data);
+ // const timeline = response.data;
+ // let output = '
Timeline Generated:
';
+ // $.each(timeline, function (date, volume) {
+ // output += `- ${date}: ${volume} emails
`;
+ // });
+ // output += '
';
+ $('#generate-timeline-result').html(response.data);
} else {
$('#generate-timeline-result').html('
Error: ' + response.data + '
');
}