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) {
Emails
+
%
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']; ?>