diff --git a/.gitignore b/.gitignore index 923d2fa..1d0a2e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /includes/vendor/ +composer.phar +/vendor/ diff --git a/css/admin-style.css b/css/admin-style.css index c011e53..580db56 100644 --- a/css/admin-style.css +++ b/css/admin-style.css @@ -1,19 +1,12 @@ /* Admin Meta Box Styles */ -#fix_deliverability_dns_issues_box, -#update_dkim_record_box, -#update_dmarc_record_box, -#update_spf_record_box { - background: #f9f9f9; +#side-sortables .postbox { +/* background: #0e0e0e;*/ border: 1px solid #ddd; - padding: 15px; margin-bottom: 15px; border-radius: 4px; } -#fix_deliverability_dns_issues_box h2, -#update_dkim_record_box h2, -#update_dmarc_record_box h2, -#update_spf_record_box h2 { +#side-sortables .postbox h2 { font-size: 16px; font-weight: bold; margin-bottom: 10px; @@ -32,7 +25,7 @@ button.button-primary:hover { color: #fff; } -.meta-box-sortables input, .meta-box-sortables textarea, .meta-box-sortables select, .meta-box-sortables input[type=number] { +.meta-box-sortables input[type=text], .meta-box-sortables textarea, .meta-box-sortables input[type=email], .meta-box-sortables select, .meta-box-sortables input[type=number] { width: 100%; max-width: 100%; } @@ -42,6 +35,33 @@ table.rl_admin_meta_table { table-layout: fixed; } +/* Campaign Timeline */ +.timeline-grid { + display: flex; + flex-wrap: wrap; + max-width: 100%; + margin: 10px 0; +} + +.timeline-grid .day { + flex: 0 0 14.28%; /* 7 rows = 100% / 7 columns */ + box-sizing: border-box; + border: 1px solid #ccc; + padding: 5px; + text-align: center; + font-size: 12px; + color: #000; +} + +.timeline-grid .day .date { + font-weight: bold; + margin-bottom: 5px; +} + +.timeline-grid .day .volume { + font-size: 14px; +} + #campaign-timeline-heatmap { width: 100%; /* Ensure it spans the available space */ height: 300px; /* Set a fixed height */ diff --git a/includes/class-rl-mailwarmer-campaign-helper.php b/includes/class-rl-mailwarmer-campaign-helper.php index 3c621ba..fe037b0 100644 --- a/includes/class-rl-mailwarmer-campaign-helper.php +++ b/includes/class-rl-mailwarmer-campaign-helper.php @@ -30,7 +30,7 @@ class RL_MailWarmer_Campaign_Helper explode("\n", $holidays_raw) ); - $min_starting_volume = (int) get_field('min_starting_email_volume', 'option') ?: 10; + $min_starting_volume = (int) get_field('min_starting_email_volume', 'option') ?: 5; $max_daily_volume = (int) get_field('max_campaign_daily_email_volume', 'option') ?: 1000; // Calculate starting daily volume (2.5% of target volume) @@ -41,8 +41,12 @@ class RL_MailWarmer_Campaign_Helper $total_days = $warmup_period * 7; // Total days in the campaign $start_date = new DateTime($start_date); + + // Calculate daily ramp-up rate - $daily_increase = ($target_volume - $starting_daily_volume) / $total_days; + $daily_increase = ($target_volume - $starting_daily_volume) / ($total_days * .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"); // Generate timeline for ($day = 0; $day < $total_days; $day++) { @@ -53,193 +57,907 @@ 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) ? rand(65, 82) / 100 : 1; + $reduction_factor = ($is_weekend || $is_holiday) ? mt_rand(65, 82) / 100 : 1; // Calculate daily volume $daily_volume = min( ceil(($starting_daily_volume + ($daily_increase * $day)) * $reduction_factor), - $target_volume * 1.2 + 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 ); + }*/ + if ($daily_volume > $max_daily_volume) { + 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] = $daily_volume; + $timeline[$date_formatted] = [ 'target_volume' => $daily_volume, 'current_volume' => 0, 'items_sent' => 0 ]; + // 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); - // log_to_file("Timeline JSON: $timeline_json"); - update_post_meta($campaign_id, 'campaign_timeline', $timeline_json); + // $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)); + + return $timeline; } - /** - * Generate a conversation for a campaign. - * - * @param int $campaign_id The ID of the campaign. - * @param array $conversation_steps Blueprint of the conversation (participants, replies, etc.). - * @param string $prompt AI prompt for generating email content. - * - * @return int|WP_Error The ID of the created conversation or a WP_Error on failure. - */ - public static function generate_conversation($campaign_id, $conversation_steps, $prompt) { - if (empty($campaign_id) || empty($conversation_steps)) { - return new WP_Error('invalid_data', __('Invalid campaign or conversation steps.', 'rl-mailwarmer')); + + public static function fill_campaign_timeline(int $campaign_id, array $campaign_timeline): array { + $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 + ], + ]; + $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 + $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"); + + // // 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) { + $timestamp = strtotime($date); + $year = date('o', $timestamp); // ISO year + $week = date('W', $timestamp); // ISO week + $week_key = sprintf('%d-%02d', $year, $week); + + if (!isset($weekly_volumes[$week_key])) { + $weekly_volumes[$week_key] = 0; + } + $weekly_volumes[$week_key] += $data['target_volume']; } - // Insert the conversation into the database + // Process each week + foreach ($weekly_volumes as $week => $volume) { + log_to_file("fill_campaign_timeline - Week $week goal: $volume"); + $weekly_scheduled_messages = 0; + + 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; + $num_conversations = ceil($volume * $percent); + log_to_file("fill_campaign_timeline - Generating $num_conversations $length conversations"); + + // Skip if no conversations to generate + if ($num_conversations === 0) continue; + + // Generate conversations + for ($i = 0; $i < $num_conversations; $i++) { + // Check if weekly volume reached + if ($weekly_scheduled_messages >= $volume) { + break 2; // Break both loops + } + + // Generate random number of responses for this conversation + $num_responses = mt_rand($ratio['num_responses_lower'], $ratio['num_responses_upper']); + + // Get week start date + list($year, $weekNum) = explode('-', $week); + $week_start = date('Y-m-d', strtotime($year . 'W' . $weekNum)); + + if ($week_start < $start_date) { + $week_start = $start_date; + } + + // Generate conversation blueprint + $conversation_steps = self::generate_conversation_blueprint( + $campaign_id, + $num_responses, + $week_start, + $end_date, + $filled_dates + ); + + // Update timeline volumes + foreach ($conversation_steps as $step) { + $step_date = date('Y-m-d', strtotime($step['scheduled_for'])); + if (isset($campaign_timeline[$step_date])) { + $campaign_timeline[$step_date]['current_volume']++; + $weekly_scheduled_messages++; + + // Check if date is now filled + if ($campaign_timeline[$step_date]['current_volume'] >= + $campaign_timeline[$step_date]['target_volume']) { + $filled_dates[] = $step_date; + } + } + } + } + } + } + + // log_to_file("fill_campaign_timeline - Campaign Timeline without single-step conversations: ", $campaign_timeline); + + // Fill remaining capacity with single-message conversations + log_to_file("fill_campaign_timeline - Filling remaining days with single conversations"); + foreach ($campaign_timeline as $date => $data) { + $remaining = $data['target_volume'] - $data['current_volume']; + // log_to_file("fill_campaign_timeline - $date remaining: $remaining"); + + while ($remaining > 0) { + $conversation_steps = self::generate_conversation_blueprint( + $campaign_id, + 0, + $date, + $date + ); + + foreach ($conversation_steps as $step) { + $step_date = date('Y-m-d', strtotime($step['scheduled_for'])); + if (isset($campaign_timeline[$step_date])) { + $campaign_timeline[$step_date]['current_volume']++; + $remaining--; + + if ($campaign_timeline[$step_date]['current_volume'] >= + $campaign_timeline[$step_date]['target_volume']) { + $filled_dates[] = $step_date; + } + } + } + } + } + + return $campaign_timeline; + } + + + + + + + /** + * 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 messages for a conversation. + * Generate AI content for the conversation using OpenAI's ChatGPT API. * - * @param int $conversation_id The ID of the conversation. - * @param int $campaign_id The ID of the campaign. - * @param array $steps The steps of the conversation. - * - * @return array An array of message IDs or WP_Error on failure. + * @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_messages($conversation_id, $campaign_id, $steps) { - if (empty($conversation_id) || empty($campaign_id) || empty($steps)) { - return new WP_Error('invalid_data', __('Invalid conversation or steps.', 'rl-mailwarmer')); + 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')); } - $message_ids = []; + 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, + ]); - foreach ($steps as $step) { - $scheduled_for = $step['scheduled_for']; - $from_email = $step['from']; - $to_email = implode(',', $step['to']); - $cc = isset($step['cc']) ? implode(',', $step['cc']) : null; - $subject = $step['subject']; - $body = $step['body']; - - // Insert the message into the database - $message_id = RL_MailWarmer_DB_Helper::insert_message( - $campaign_id, - $conversation_id, - $scheduled_for, - $from_email, - $to_email, - $cc, - $subject, - $body - ); - - if (!$message_id) { - return new WP_Error('db_error', __('Failed to create message.', 'rl-mailwarmer')); - } - - $message_ids[] = $message_id; + return $response['choices'][0]['message']['content']; + } catch (\Exception $e) { + return new WP_Error('api_error', $e->getMessage()); } - - return $message_ids; } /** - * Generate conversations and messages for a campaign. + * 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); + + 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 int $weekly_volume The target weekly email volume. - * - * @return array An array of created conversation IDs or WP_Error on failure. + * @param string $pool The name of the pool. Valid options are: cc, reply + * @return array The email addresses to include in the CC pool. */ - public static function generate_conversations($campaign_id, $weekly_volume) { - $conversation_ids = []; + 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); - // Example: Break down weekly volume into conversation ratios - $conversation_ratios = [ - ['percentage' => 5, 'participants' => [3, 6], 'replies' => [5, 12]], - ['percentage' => 10, 'participants' => [3, 5], 'replies' => [3, 5]], - ['percentage' => 15, 'participants' => [2, 4], 'replies' => [1, 3]], - ['percentage' => 70, 'participants' => [2, 6], 'replies' => [0, 0]], - ]; + if (empty($domain_id)) { + return []; // Return an empty array if no domain is found + } - foreach ($conversation_ratios as $ratio) { - $volume = ceil($weekly_volume * ($ratio['percentage'] / 100)); + // 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 + ]; + } - for ($i = 0; $i < $volume; $i++) { - $conversation_steps = []; // Generate steps based on ratio - $prompt = ''; // Generate prompt for AI (to be implemented) + $query = new WP_Query($args); - // Create conversation - $conversation_id = self::generate_conversation($campaign_id, $conversation_steps, $prompt); + $email_pool = []; - if (is_wp_error($conversation_id)) { - return $conversation_id; + 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; } - - $steps = []; // Generate steps for this conversation - $message_result = self::generate_messages($conversation_id, $campaign_id, $steps); - - if (is_wp_error($message_result)) { - return $message_result; - } - - $conversation_ids[] = $conversation_id; } } - return $conversation_ids; - } - - - /** - * Generate additional email accounts for the campaign. - * - * Creates additional email accounts for initiating email conversations, - * replying to chains, and being included on CCs to increase email volume. - * - * @param int $campaign_id The campaign post ID. - * @return array List of generated email accounts. - */ - public static function generate_campaign_email_accounts($campaign_id) - { - // Implementation placeholder - return []; + return $email_pool; } /** - * Generate conversation vectors for the campaign. + * Add timestamps to parsed conversation steps. * - * Creates a detailed map of email chains, participants, and timelines, - * based on campaign requirements. - * - * @param int $campaign_id The campaign post ID. - * @return array List of conversation vectors with timestamps and participants. + * @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 generate_campaign_conversation_vectors($campaign_id) - { - // Implementation placeholder - return []; + 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 email conversation content using ChatGPT. + * Generate a semi-randomized timestamp with a random date and time. * - * Creates realistic email conversations with context and replies, - * leveraging ChatGPT for natural language generation. - * - * @param int $campaign_id The campaign post ID. - * @param array $vector Details of the conversation vector (participants, topics, etc.). - * @return string Generated email conversation content. + * @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. */ - public static function generate_email_conversation($campaign_id, $vector) - { - // Implementation placeholder - return ''; + 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; + } + + + + + + } /** @@ -322,89 +1040,448 @@ add_action('wp_ajax_rl_mailwarmer_generate_timeline', function () { } }); -// Cal-Heatmap +/* + * Generate Conversation Metabox + * + */ - -add_action('edit_form_after_title', function ($post) { - if ($post->post_type === 'campaign') { - echo '
'; - } +add_action('add_meta_boxes', function () { + add_meta_box( + 'generate_conversation_box', // Unique ID for the meta box + __('Generate Conversation', 'rl-mailwarmer'), // Title of the meta box + 'rl_mailwarmer_render_conversation_box', // Callback to display the box content + 'campaign', // Post type + 'side', // Context: side, normal, or advanced + 'default' // Priority + ); }); + +/** + * Render the Generate Conversation meta box. + * + * @param WP_Post $post The current post object. + */ +function rl_mailwarmer_render_conversation_box($post) { + // Add a nonce for security + wp_nonce_field('rl_generate_conversation_nonce', 'rl_generate_conversation_nonce_field'); + + // Meta box form + ?> +
+ + + + + + + + + + +
+ post_type === 'campaign') { - // Enqueue D3.js (required for Cal-Heatmap) wp_enqueue_script( - 'd3-js', - 'https://d3js.org/d3.v7.min.js', - [], - '7.0.0', - true - ); - - // Enqueue Cal-Heatmap - wp_enqueue_script( - 'cal-heatmap', - 'https://unpkg.com/cal-heatmap/dist/cal-heatmap.min.js', - ['d3-js'], - '4.2.4', - true - ); - - // Enqueue Cal-Heatmap CSS - wp_enqueue_style( - 'cal-heatmap', - 'https://unpkg.com/cal-heatmap/dist/cal-heatmap.css', - [], - '4.2.4' - ); - - // Custom heatmap script - wp_enqueue_script( - 'rl-mailwarmer-campaign-heatmap', - RL_MAILWARMER_URL . 'js/campaign-timeline-heatmap.js', - ['cal-heatmap', 'jquery'], + 'rl-generate-conversation', + RL_MAILWARMER_URL . 'js/generate-conversation.js', + ['jquery'], null, true ); - // Pass data to the script - wp_localize_script('rl-mailwarmer-campaign-heatmap', 'rlMailWarmerHeatmap', [ + wp_localize_script('rl-generate-conversation', 'rlGenerateConversation', [ 'ajax_url' => admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('campaign_timeline_nonce'), + 'nonce' => wp_create_nonce('rl_generate_conversation_nonce'), 'post_id' => $post->ID, ]); } }); -add_action('wp_ajax_rl_mailwarmer_get_timeline', function () { - // Verify nonce - if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'campaign_timeline_nonce')) { - wp_send_json_error(__('Invalid nonce', 'rl-mailwarmer')); +add_action('wp_ajax_rl_generate_conversation', function () { + check_ajax_referer('rl_generate_conversation_nonce', 'nonce'); + + $campaign_id = intval($_POST['post_id']); + $args = [ + 'initiated_by' => sanitize_text_field($_POST['initiated_by'] ?? ''), + 'subject' => sanitize_text_field($_POST['subject'] ?? ''), + 'length' => sanitize_text_field($_POST['length'] ?? ''), + ]; + + $result = RL_MailWarmer_Campaign_Helper::generate_conversation($campaign_id, $args); + + if (is_wp_error($result)) { + wp_send_json_error(['message' => $result->get_error_message()]); } - // Validate and sanitize input - $post_id = intval($_POST['post_id']); - if (!$post_id || get_post_type($post_id) !== 'campaign') { - wp_send_json_error(__('Invalid campaign ID.', 'rl-mailwarmer')); - } - - // Retrieve the timeline - $timeline_json = get_post_meta($post_id, 'campaign_timeline', true); - $timeline = json_decode($timeline_json, true); - - if (!$timeline) { - wp_send_json_error(__('No timeline data found.', 'rl-mailwarmer')); - } - - // Convert timeline to Cal-Heatmap format (timestamp => value) - $heatmap_data = []; - foreach ($timeline as $date => $volume) { - $timestamp = strtotime($date); // Convert to UNIX timestamp - $heatmap_data[$timestamp] = $volume; - } - - wp_send_json_success($heatmap_data); + wp_send_json_success(['conversation_id' => $result]); }); + + +/** + * Add the "Generate Blueprint" metabox to the Campaign edit page. + */ +add_action('add_meta_boxes', function () { + add_meta_box( + 'rl_mailwarmer_generate_blueprint', + __('Generate Blueprint', 'rl-mailwarmer'), + 'rl_mailwarmer_render_generate_blueprint_metabox', + 'campaign', // Post type + 'side', + 'default' + ); +}); + +/** + * Render the "Generate Blueprint" metabox. + * + * @param WP_Post $post The current post object. + */ +function rl_mailwarmer_render_generate_blueprint_metabox($post) { + wp_nonce_field('rl_mailwarmer_generate_blueprint_nonce', 'rl_mailwarmer_generate_blueprint_nonce_field'); + + ?> + + + +
+ + + __('Invalid nonce.', 'rl-mailwarmer')]); + } + + // Check permissions + if (!current_user_can('edit_post', $_POST['post_id'])) { + wp_send_json_error(['message' => __('Permission denied.', 'rl-mailwarmer')]); + } + + // Validate inputs + $post_id = intval($_POST['post_id']); + $length = intval($_POST['length']); + if ($length < 1) { + wp_send_json_error(['message' => __('Invalid length specified.', 'rl-mailwarmer')]); + } + + try { + // Generate the blueprint + RL_MailWarmer_Campaign_Helper::generate_conversation_blueprint($post_id, $length); + wp_send_json_success(); + } catch (Exception $e) { + wp_send_json_error(['message' => $e->getMessage()]); + } +}); + + + + +/** + * Add a flexbox grid displaying the campaign timeline with gradient colors based on intensity. + */ +add_action('edit_form_after_title', 'rl_mailwarmer_display_campaign_timeline'); + +function rl_mailwarmer_display_campaign_timeline($post) { + // log_to_file("rl_mailwarmer_display_campaign_timeline - Running for {$post->post_title}"); + if ($post->post_type !== 'campaign') { + return; + } + + // Fetch campaign timeline + $campaign_timeline = get_post_meta($post->ID, 'campaign_timeline', true); + if (empty($campaign_timeline)) { + echo '

' . esc_html__('No timeline available for this campaign.', 'rl-mailwarmer') . '

'; + return; + } + + $campaign_timeline = json_decode($campaign_timeline, true); + + // Organize timeline by weeks for grid structure + $weeks = []; + $max_volume = 0; + foreach ($campaign_timeline as $date => $day) { + $max_volume = max($max_volume, $day['target_volume']); + $week_number = date('W', strtotime($date)); + if (!isset($weeks[$week_number])) { + $weeks[$week_number] = []; + } + $weeks[$week_number][$date] = $day; + } + // log_to_file("edit_form_after_title - Weeks array: ", $weeks); + + // Add padding days and flatten + $grid_columns = []; + foreach ($weeks as $week_number => $week) { + // Get the first date of this week + $first_date = array_key_first($week); + $first_day_num = date('N', strtotime($first_date)); // 1 (Monday) through 7 (Sunday) + + // Add padding days before if needed + if ($first_day_num > 1) { + $padding_days = $first_day_num - 1; + $monday_date = date('Y-m-d', strtotime("-{$padding_days} days", strtotime($first_date))); + + for ($i = 0; $i < $padding_days; $i++) { + $pad_date = date('Y-m-d', strtotime("+{$i} days", strtotime($monday_date))); + $grid_columns[$pad_date] = [ + 'target_volume' => 0, + 'is_padding' => true + ]; + } + } + + // Add actual days + foreach ($week as $date => $day) { + $day['is_padding'] = false; + $grid_columns[$date] = $day; + } + } + + // log_to_file("rl_mailwarmer_display_campaign_timeline - Grid Columns: ", $grid_columns); + + $dates = array_keys($grid_columns); + $volumes = array_map(function($day) { + return (isset($day['is_padding']) && $day['is_padding'] == true ) ? 0 : $day; + }, $grid_columns); + + // log_to_file("rl_mailwarmer_display_campaign_timeline - Grid volumes: ", $volumes); + + $chart_data = array_map(function($date, $volume) { + return [ + 'date' => date('M d', strtotime($date)), + 'target' => isset($volume['target_volume']) ? $volume['target_volume'] : null , + 'sent' => isset($volume['items_sent']) ? $volume['items_sent'] : null + ]; + }, $dates, $volumes); + + // log_to_file("Chart Data: ", $chart_data); + + // Gradient colors array + // $gradients = ["#f12711", "#f24913", "#f36b15", "#f48d17", "#f5af19", "#f8c353", "#fad78c", "#fdebc6", "#ffffff"]; + $gradients = ["#ffffff", "#fdebc6", "#fad78c", "#f8c353", "#f5af19", "#f48d17", "#f36b15", "#f24913", "#f12711"]; + + + // Generate the grid + ?> + +
+ $day): + if (!empty($day['is_padding'])): + ?> +
+
+
-
+
+ +
+
+
Emails
+
+ +
+ + + + + post_type === 'campaign') { +// echo '
'; +// } +// }); +// add_action('admin_enqueue_scripts', function ($hook) { +// global $post; + +// if (($hook === 'post.php' || $hook === 'post-new.php') && $post->post_type === 'campaign') { +// // Enqueue D3.js (required for Cal-Heatmap) +// wp_enqueue_script( +// 'd3-js', +// 'https://d3js.org/d3.v7.min.js', +// [], +// '7.0.0', +// true +// ); + +// // Enqueue Cal-Heatmap +// wp_enqueue_script( +// 'cal-heatmap', +// 'https://unpkg.com/cal-heatmap/dist/cal-heatmap.min.js', +// ['d3-js'], +// '4.2.4', +// true +// ); + +// // Enqueue Cal-Heatmap CSS +// wp_enqueue_style( +// 'cal-heatmap', +// 'https://unpkg.com/cal-heatmap/dist/cal-heatmap.css', +// [], +// '4.2.4' +// ); + +// // Custom heatmap script +// wp_enqueue_script( +// 'rl-mailwarmer-campaign-heatmap', +// RL_MAILWARMER_URL . 'js/campaign-timeline-heatmap.js', +// ['cal-heatmap', 'jquery'], +// null, +// true +// ); + +// // Pass data to the script +// wp_localize_script('rl-mailwarmer-campaign-heatmap', 'rlMailWarmerHeatmap', [ +// 'ajax_url' => admin_url('admin-ajax.php'), +// 'nonce' => wp_create_nonce('campaign_timeline_nonce'), +// 'post_id' => $post->ID, +// ]); +// } +// }); + +// add_action('wp_ajax_rl_mailwarmer_get_timeline', function () { +// // Verify nonce +// if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'campaign_timeline_nonce')) { +// wp_send_json_error(__('Invalid nonce', 'rl-mailwarmer')); +// } + +// // Validate and sanitize input +// $post_id = intval($_POST['post_id']); +// if (!$post_id || get_post_type($post_id) !== 'campaign') { +// wp_send_json_error(__('Invalid campaign ID.', 'rl-mailwarmer')); +// } + +// // Retrieve the timeline +// $timeline_json = get_post_meta($post_id, 'campaign_timeline', true); +// $timeline = json_decode($timeline_json, true); + +// if (!$timeline) { +// wp_send_json_error(__('No timeline data found.', 'rl-mailwarmer')); +// } + +// // Convert timeline to Cal-Heatmap format (timestamp => value) +// $heatmap_data = []; +// foreach ($timeline as $date ) { +// $timestamp = "'" . strtotime($date['date']) . "'"; // Convert to UNIX timestamp +// $heatmap_data[$timestamp] = $date['target_volume']; +// } + +// wp_send_json_success($timeline); +// }); diff --git a/includes/class-rl-mailwarmer-db-helper.php b/includes/class-rl-mailwarmer-db-helper.php index 3aa37ab..732b257 100644 --- a/includes/class-rl-mailwarmer-db-helper.php +++ b/includes/class-rl-mailwarmer-db-helper.php @@ -2,8 +2,10 @@ class RL_MailWarmer_DB_Helper { - private static $conversation_table = 'rl_mailwarmer_conversation'; - private static $message_table = 'rl_mailwarmer_message'; + private static $conversations_table = 'rl_mailwarmer_conversations'; + private static $messages_table = 'rl_mailwarmer_messages'; + private static $backups_table = 'rl_mailwarmer_dns_backups'; + private static $health_reports_table = 'rl_mailwarmer_health_reports'; /** * Create necessary database tables. @@ -14,18 +16,22 @@ class RL_MailWarmer_DB_Helper { $charset_collate = $wpdb->get_charset_collate(); // Conversation table - $conversation_sql = "CREATE TABLE IF NOT EXISTS `{$wpdb->prefix}" . self::$conversation_table . "` ( + $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, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - conversation_steps JSON NOT NULL, - prompt TEXT 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, PRIMARY KEY (id), - INDEX campaign_id_idx (campaign_id) + KEY campaign_id_idx (campaign_id), + KEY status_idx (status), + KEY first_message_timestamp_idx (first_message_timestamp) ) $charset_collate;"; // Message table - $message_sql = "CREATE TABLE IF NOT EXISTS `{$wpdb->prefix}" . self::$message_table . "` ( + $message_sql = "CREATE TABLE IF NOT EXISTS `{$wpdb->prefix}" . self::$messages_table . "` ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, campaign_id BIGINT(20) UNSIGNED NOT NULL, conversation_id BIGINT(20) UNSIGNED NOT NULL, @@ -35,33 +41,61 @@ class RL_MailWarmer_DB_Helper { to_email TEXT NOT NULL, cc TEXT NULL, subject VARCHAR(255) NOT NULL, - body TEXT NOT NULL, + body LONGTEXT NOT NULL, PRIMARY KEY (id), INDEX scheduled_idx (scheduled_for_timestamp, status), INDEX conversation_id_idx (conversation_id), INDEX campaign_id_idx (campaign_id) ) $charset_collate;"; + // Backup table + $backup_sql = "CREATE TABLE IF NOT EXISTS `{$wpdb->prefix}" . self::$backups_table . "` ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + domain_id BIGINT UNSIGNED NOT NULL, + record_name VARCHAR(255) NOT NULL, + record_type VARCHAR(50) NOT NULL, + record_content LONGTEXT NOT NULL, + created_at DATETIME NOT NULL, + INDEX (domain_id), + INDEX (record_name), + INDEX (record_type) + ) $charset_collate;"; + + // Backup table + $health_report_sql = "CREATE TABLE IF NOT EXISTS `{$wpdb->prefix}" . self::$health_reports_table . "` ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + domain_id BIGINT UNSIGNED NOT NULL, + report_content LONGTEXT NOT NULL, + created_at DATETIME NOT NULL, + last_checked DATETIME NOT NULL, + INDEX (domain_id) + ) $charset_collate;"; + + // DNS Backup table + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($conversation_sql); dbDelta($message_sql); + dbDelta($backup_sql); + dbDelta($health_report_sql); } /** * Insert a conversation record. */ - public static function insert_conversation($campaign_id, $conversation_steps, $prompt) { + public static function insert_conversation($conversation_data) { global $wpdb; - $wpdb->insert( - "{$wpdb->prefix}" . self::$conversation_table, - [ - 'campaign_id' => $campaign_id, - 'conversation_steps' => json_encode($conversation_steps), - 'prompt' => $prompt, - ], - ['%d', '%s', '%s'] - ); + // $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; } @@ -69,28 +103,33 @@ class RL_MailWarmer_DB_Helper { /** * Insert a message record. */ - public static function insert_message($campaign_id, $conversation_id, $scheduled_for, $from_email, $to_email, $cc, $subject, $body) { + public static function insert_message($message_data) { global $wpdb; - $wpdb->insert( - "{$wpdb->prefix}" . self::$message_table, - [ - 'campaign_id' => $campaign_id, - 'conversation_id' => $conversation_id, - 'scheduled_for_timestamp' => $scheduled_for, - 'status' => 'pending', - 'from_email' => $from_email, - 'to_email' => $to_email, - 'cc' => $cc, - 'subject' => $subject, - 'body' => $body, - ], - ['%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s'] - ); + // log_to_file("insert_message - Message body: $body"); + + // $wpdb->insert( + // "{$wpdb->prefix}rl_mailwarmer_message", + // [ + // 'campaign_id' => $campaign_id, + // 'conversation_id' => $conversation_id, + // 'scheduled_for_timestamp' => $scheduled_for, + // 'status' => 'pending', + // 'from_email' => $from_email, + // 'to_email' => $to_email, + // 'cc' => $cc, + // 'subject' => $subject, + // 'body' => $body, + // 'first_message' => $first_message ? 1 : 0, + // ], + // ['%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d'] + // ); + $wpdb->insert("{$wpdb->prefix}" . self::$messages_table, $message_data); return $wpdb->insert_id; } + /** * Fetch pending messages. */ @@ -98,7 +137,7 @@ class RL_MailWarmer_DB_Helper { global $wpdb; $sql = $wpdb->prepare( - "SELECT * FROM `{$wpdb->prefix}" . self::$message_table . "` + "SELECT * FROM `{$wpdb->prefix}" . self::$messages_table . "` WHERE scheduled_for_timestamp <= %s AND status = 'pending' LIMIT %d", current_time('mysql'), @@ -107,5 +146,169 @@ class RL_MailWarmer_DB_Helper { return $wpdb->get_results($sql, ARRAY_A); } + + // Get Message Counts + public static function get_message_counts_by_date($campaign_id) { + global $wpdb; + $messages_table = $wpdb->prefix . self::$messages_table; + + $results = $wpdb->get_results( + $wpdb->prepare( + "SELECT + DATE(scheduled_for_timestamp) AS message_date, + COUNT(*) AS message_count + FROM + $messages_table + WHERE + campaign_id = %d + GROUP BY + DATE(scheduled_for_timestamp) + ORDER BY + message_date ASC", + $campaign_id + ), + ARRAY_A + ); + + return $results; + } + + /** + * Insert DNS backups into the database. + * + * @param mixed $domain The domain ID or post object. + * @param array $records The DNS records to back up. + * @return bool True on success, false on failure. + */ + public static function insert_dns_backup($domain, $record) { + global $wpdb; + $backups_table = $wpdb->prefix . 'rl_mailwarmer_dns_backups'; + + $domain_post = is_object($domain) ? $domain : RL_MailWarmer_Domain_Helper::get_domain_post($domain); + if (!$domain_post) { + throw new Exception(__('Invalid domain specified.', 'rl-mailwarmer')); + } + + // log_to_file("insert_dns_backup - Attemting to backup record: ", $record); + + $existing = $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM $backups_table WHERE domain_id = %d AND record_name = %s AND record_type = %s ORDER BY created_at DESC LIMIT 1", + $domain_post->ID, + $record['name'], + $record['type'] + ), + ARRAY_A + ); + + if ($existing && $existing['record_content'] === $record['content']) { + // log_to_file("insert_dns_backup - New & Old content are the same. Skipping insert and returning TRUE"); + // return true; + return $existing['id']; + // continue; // Skip unchanged records + } + + try { + $wpdb->insert($backups_table, [ + 'domain_id' => $domain_post->ID, + 'record_name' => $record['name'], + 'record_type' => $record['type'], + 'record_content' => $record['content'], + 'created_at' => current_time('mysql'), + ]); + } catch (Exception $e) { + error_log(__('insert_dns_backup - Failed to insert new DNS backup record: ', 'rl-mailwarmer') . $e->getMessage()); + } + + + return $wpdb->insert_id; + // return true; + } + + /** + * Insert health report backups into the database. + * + * @param mixed $domain The domain ID or post object. + * @param array $records The DNS records to back up. + * @return bool True on success, false on failure. + */ + public static function insert_health_report_backup($domain, $report) { + global $wpdb; + $health_reports_table = $wpdb->prefix . 'rl_mailwarmer_health_reports'; + + $domain_post = is_object($domain) ? $domain : RL_MailWarmer_Domain_Helper::get_domain_post($domain); + if (!$domain_post) { + throw new Exception(__('Invalid domain specified.', 'rl-mailwarmer')); + } + + // log_to_file("insert_health_report_backup - Attempting to save health report for {$domain_post->post_title}: "); + + try { + // log_to_file("insert_health_report_backup - Fetching existing rows."); + $existing = $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM $health_reports_table WHERE domain_id = %d ORDER BY created_at DESC LIMIT 1", + $domain_post->ID + ), + ARRAY_A + ); + // log_to_file("insert_health_report_backup - Done fetching existing rows."); + if ($existing && $existing['report_content'] === $report) { + // log_to_file("insert_health_report_backup - New & Old content are the same. Skipping insert and returning ID of existing record"); + // Prepare the data + $data = [ + 'last_checked' => current_time('mysql'), // Save the JSON response as a string + ]; + $where = [ + 'id' => $existing['id'], + ]; + + // Update the database + try { + $result = $wpdb->update( + $health_reports_table, + $data, + $where, + ['%s'], + ['%d'] + ); + } catch (Exception $e) { + log_to_file("insert_health_report_backup - Error updating existing database entry."); + throw new Exception(__('insert_health_report_backup - Error updating existing database entry: ', 'rl-mailwarmer') . $e->getMessage()); + } + return $existing['id']; + // continue; // Skip unchanged records + } + } catch (Exception $e) { + log_to_file("insert_health_report_backup - Error fetching existing rows."); + throw new Exception(__('insert_health_report_backup - Unable to fetch existing records: ', 'rl-mailwarmer') . $e->getMessage()); + + } + + try { + $current_time = current_time('mysql'); + $data = [ + 'domain_id' => $domain_post->ID, + 'report_content' => $report, + 'created_at' => $current_time, + 'last_checked' => $current_time, + ]; + $result = $wpdb->insert( + $health_reports_table, + $data + ); + if ($result === false) { + return new WP_Error('db_update_failed', __('Failed to update the conversation steps.', 'rl-mailwarmer')); + } + return $result; + } catch (Exception $e) { + throw new Exception(__('insert_health_report_backup - Failed to insert new health report record: ', 'rl-mailwarmer') . $e->getMessage()); + // error_log(__('insert_dns_backup - Failed to insert new health report record: ', 'rl-mailwarmer') . $e->getMessage()); + } + + + + return false; + } } \ No newline at end of file diff --git a/includes/class-rl-mailwarmer-domain-helper.php b/includes/class-rl-mailwarmer-domain-helper.php index 953fdc8..598c359 100644 --- a/includes/class-rl-mailwarmer-domain-helper.php +++ b/includes/class-rl-mailwarmer-domain-helper.php @@ -9,87 +9,164 @@ if (!defined('ABSPATH')) { use GuzzleHttp\Client; -require 'vendor/autoload.php'; // Ensure you install Guzzle via Composer +class RL_MailWarmer_Domain_Helper { - -class RL_MailWarmer_Domain_Helper -{ /** - * Normalize the domain input (post object, array, or ID). + * Get the domain post object by ID or name. * - * @param mixed $domain The domain input. - * @return WP_Post|null The domain post object or null if invalid. + * @param mixed $domain The domain ID or name. + * @return WP_Post|null The domain post object or null if not found. */ - private static function get_domain_post($domain) - { - if (is_numeric($domain)) { - return get_post($domain); - } elseif (is_array($domain) && isset($domain['ID'])) { - return get_post($domain['ID']); - } elseif ($domain instanceof WP_Post) { + public static function get_domain_post($domain) { + if ( is_a( $domain, 'WP_Post' ) ) { + // log_to_file("get_domain_post - Already a WP Post"); return $domain; + } else if (is_numeric($domain)) { + // log_to_file("get_domain_post - Fetching WP Post by ID"); + return get_post($domain); } - - return null; + // log_to_file("get_domain_post - Fetching WP Post by Title"); + return get_page_by_title($domain, OBJECT, 'domain'); } /** - * Get Cloudflare API credentials from the domain post. + * Get CloudFlare credentials from the domain post. * * @param WP_Post $domain_post The domain post object. - * @return array|null Array with email and key or null if missing. + * @return array Associative array containing `api_email`, `api_key`, and `zone_id`. + * @throws Exception If credentials are missing. */ - private static function get_cloudflare_credentials(WP_Post $domain_post) - { - $email = get_post_meta($domain_post->ID, 'cloudflare_api_email', true); - $key = get_post_meta($domain_post->ID, 'cloudflare_api_key', true); + public static function get_cloudflare_credentials($domain) { + $domain_post = self::get_domain_post($domain); + $domain_name = $domain_post->post_title; + $api_email = get_post_meta($domain_post->ID, 'cloudflare_api_email', true); + $api_key = get_post_meta($domain_post->ID, 'cloudflare_api_key', true); + $zone_id = get_post_meta($domain_post->ID, 'cloudflare_zone_id') ? get_post_meta($domain_post->ID, 'cloudflare_zone_id', true) : false; + + // If either credential is missing from domain, check user meta + if (!$api_email || !$api_key) { + $owner_id = get_post_meta($domain_post->ID, 'owner_id', true); + // log_to_file("get_cloudflare_credentials - API credentials not saved to domain. Trying to fetch them from the owner: $owner_id"); + if ($owner_id) { + if (!$api_email) { + $api_email = get_user_meta($owner_id, 'cloudflare_api_email', true); + } + if (!$api_key) { + $api_key = get_user_meta($owner_id, 'cloudflare_api_key', true); + } + } else { + throw new Exception(__("get_cloudflare_credentials - CloudFlare credentials are incomplete for domain: {$domain_post->ID}.", "rl-mailwarmer")); + } + } + + // Fetch & save the Zone ID if we don't already have it + if (!$zone_id) { + $client = new \GuzzleHttp\Client([ + 'base_uri' => 'https://api.cloudflare.com/client/v4/', + 'headers' => [ + 'Content-Type' => 'application/json', + ], + ]); + + // log_to_file(" - Client API Key: ", $client->getConfig('api_key')); + + try { + $response = $client->get('zones', [ + 'headers' => [ + 'Authorization' => "Bearer {$api_key}", + 'Content-Type' => 'application/json', + ], + 'query' => ['name' => $domain_name], + ]); + + $data = json_decode($response->getBody()->getContents(), true); + + if (isset($data['result'][0]['id'])) { + $zone_id = $data['result'][0]['id']; + update_post_meta($domain_post->ID,'cloudflare_zone_id',$zone_id); + log_to_file("get_cloudflare_credentials - Saved Zone ID to $domain_name: $zone_id"); + + } + + // throw new Exception(__('get_cloudflare_credentials - Zone ID not found for the domain.', 'rl-mailwarmer')); + } catch (Exception $e) { + throw new Exception(__('get_cloudflare_credentials - Failed to fetch CloudFlare zone ID: ', 'rl-mailwarmer') . $e->getMessage()); + } + } + + if (!$api_email || !$api_key || !$zone_id) { + return false; + } else { + return compact('api_email', 'api_key', 'zone_id'); + } - return $email && $key ? ['email' => $email, 'key' => $key] : null; } /** - * Fetch DNS records using the Cloudflare API. + * Establish a connection to the CloudFlare API. + * + * @param array $credentials The CloudFlare credentials. + * @return GuzzleHttp\Client The Guzzle client for API requests. + */ + private static function get_cloudflare_client($domain) { + $domain_post = self::get_domain_post($domain); + if (!$domain_post) { + throw new Exception(__('get_cloudflare_client - Invalid domain specified.', 'rl-mailwarmer')); + } + + $domain_name = $domain_post->post_title; + $credentials = self::get_cloudflare_credentials($domain_post); + + + $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; + + + } + + /** + * Fetch DNS records using the CloudFlare API. * * @param string $domain The domain name. * @param string $type The record type (e.g., TXT, A). - * @param array $credentials Cloudflare credentials. * @return array The DNS records. + * @throws Exception If fetching records fails. */ - private static function fetch_dns_records($domain, $type, $credentials) - { + private static function fetch_dns_records($client, $type = null) { - log_to_file("fetch_dns_records - Fetching records for $domain"); - $endpoint = "https://api.cloudflare.com/client/v4/zones"; - $client = new \GuzzleHttp\Client(['timeout' => 10]); + try { + $response = $client->get("zones/{$client->getConfig('zone_id')}/dns_records", [ + 'headers' => [ + 'Authorization' => "Bearer {$client->getConfig('api_key')}", + 'Content-Type' => 'application/json', + ], + 'query' => ['type' => $type], + ]); - // Fetch zone ID - $response = $client->get($endpoint, [ - 'headers' => [ - 'Authorization' => "Bearer {$credentials['key']}", - 'Content-Type' => 'application/json', - ], - 'query' => ['name' => $domain], - ]); + // log_to_file("fetch_dns_records - response: ", $response); + $data = json_decode($response->getBody()->getContents(), true); - $zones = json_decode((string) $response->getBody(), true); - if (empty($zones['result'][0]['id'])) { - throw new RuntimeException('Zone ID not found for the domain.'); + if (isset($data['result']) && is_array($data['result'])) { + return $data['result']; + // log_to_file("fetch_dns_records - Data: ", $data['result']); + } + + throw new Exception(__('fetch_dns_records - No DNS records found.', 'rl-mailwarmer')); + } catch (Exception $e) { + throw new Exception(__('fetch_dns_records - Failed to fetch DNS records: ', 'rl-mailwarmer') . $e->getMessage()); } - - $zone_id = $zones['result'][0]['id']; - - // Fetch DNS records - $dns_response = $client->get("{$endpoint}/{$zone_id}/dns_records", [ - 'headers' => [ - 'Authorization' => "Bearer {$credentials['key']}", - 'Content-Type' => 'application/json', - ], - 'query' => ['type' => $type], - ]); - - return json_decode((string) $dns_response->getBody(), true)['result'] ?? []; } + /** * Update or create a DNS record using the Cloudflare API. * @@ -99,69 +176,40 @@ class RL_MailWarmer_Domain_Helper * @param string $name The record name. * @param string $content The record content. * @param int $ttl The TTL value. - * @param array $credentials Cloudflare credentials. * @return bool True on success. */ - private static function update_dns_record($domain_id, $domain, $type, $name, $content, $ttl, $credentials) + private static function update_dns_record($domain, $domain_name, $type, $name, $content, $ttl, $priority = 0) { - // log_to_file("Updating $name record on $domain with: " . $content); - $domain_post = self::get_domain_post($domain_id); + // log_to_file("update_dns_record - Updating $name record on $domain_name with: " . $content); + $domain_post = self::get_domain_post($domain); if (!$domain_post) { // log_to_file("Invalid domain input"); throw new InvalidArgumentException('Invalid domain input.'); } - - $endpoint = "https://api.cloudflare.com/client/v4/zones"; - $client = new \GuzzleHttp\Client(['timeout' => 10]); - - // Fetch zone ID - $response = $client->get($endpoint, [ - 'headers' => [ - 'Authorization' => "Bearer {$credentials['key']}", - 'Content-Type' => 'application/json', - ], - 'query' => ['name' => $domain], - ]); - - $zones = json_decode((string) $response->getBody(), true); - if (empty($zones['result'][0]['id'])) { - throw new RuntimeException('Zone ID not found for the domain.'); - } - - $zone_id = $zones['result'][0]['id']; - - // Fetch existing DNS records - $dns_response = $client->get("{$endpoint}/{$zone_id}/dns_records", [ - 'headers' => [ - 'Authorization' => "Bearer {$credentials['key']}", - 'Content-Type' => 'application/json', - ], - 'query' => ['type' => $type], - ]); - - $records = json_decode((string) $dns_response->getBody(), true)['result'] ?? []; - // log_to_file("All records", $records); + $client = self::get_cloudflare_client($domain); + $dns_records = self::fetch_dns_records($client, $type); // Search existing records first, update it if found - foreach ($records as $record) { - if ($record['name'] === $name) { + foreach ($dns_records as $record) { + if (($record['name'] === $name) && ($record['type'] === $type)) { if ($content === $record['content']) { - // log_to_file("Old and new values are the same. Not updating DNS $name record on $domain with: " . $content); - return false; + log_to_file("update_dns_record - Old and new values are the same. Not updating DNS $name record on $domain with: " . $content); + return true; } - // log_to_file("Match found! Creating backup before updating"); + log_to_file("update_dns_record - Match found! Creating backup before updating"); // Backup the existing record before updating - $backup_response = self::backup_dns_record($record, $domain_post->ID); + $backup_response = RL_MailWarmer_DB_Helper::insert_dns_backup($domain, $record); + // return RL_MailWarmer_DB_Helper::insert_dns_backup($domain_post->post_title, $data['result']); // log_to_file("Backup response: $backup_response"); if ( $backup_response ){ - // log_to_file("Updating $name record on $domain with: " . $content); + log_to_file("update_dns_record - Backup successful! Updating $name record on $domain_name with: " . $content); // log_to_file("Current value: " . $record['content']); // Update existing record - $client->put("{$endpoint}/{$zone_id}/dns_records/{$record['id']}", [ + $client->put("zones/{$client->getConfig('zone_id')}/dns_records/{$record['id']}", [ 'headers' => [ - 'Authorization' => "Bearer {$credentials['key']}", + 'Authorization' => "Bearer {$client->getConfig('api_key')}", 'Content-Type' => 'application/json', ], 'json' => [ @@ -169,163 +217,167 @@ class RL_MailWarmer_Domain_Helper 'name' => $name, 'content' => $content, 'ttl' => $ttl, + 'priority'=> (int) $priority, ], ]); return true; } else { - // log_to_file("Error creating Backup DNS Record $name for $domain_post->title. NOT updating DNS record."); + log_to_file("update_dns_record - Error creating Backup DNS Record $name for $domain_post->title. NOT updating DNS record."); return false; } } } // Create new record if not found - $client->post("{$endpoint}/{$zone_id}/dns_records", [ + log_to_file("update_dns_record - Creating new record"); + $response = $client->post("zones/{$client->getConfig('zone_id')}/dns_records", [ 'headers' => [ - 'Authorization' => "Bearer {$credentials['key']}", + 'Authorization' => "Bearer {$client->getConfig('api_key')}", 'Content-Type' => 'application/json', ], 'json' => [ 'type' => $type, 'name' => $name, 'content' => $content, - 'ttl' => $ttl, + 'ttl' => (int) $ttl, + 'priority'=> (int) $priority, ], ]); + $result = json_decode($response->getBody(), true); + log_to_file("update_dns_record - Result: ", $result); return true; } - /** - * Backup DNS record before making changes. - * - * @param array $record The DNS record to backup. - * @param int $domain_id The ID of the associated domain post. - */ - private static function backup_dns_record($record, $domain_id) - { - - /* - * TODO: Move records to a separate table; only save a new backup if the changes are diff- - * erent than the last backup; add roll-back system - */ - - $post_data = [ - 'post_type' => 'dns_record_backup', - 'post_title' => $record['name'] . ' - ' . current_time('mysql'), - 'post_status' => 'private', - 'post_content'=> json_encode($record, JSON_PRETTY_PRINT), - 'meta_input' => [ - 'domain_id' => $domain_id, // Associated domain ID - 'dns_type' => $record['type'], - 'dns_name' => $record['name'], - 'dns_content' => $record['content'], - 'dns_ttl' => $record['ttl'], - ], - ]; - - // Insert the post into WordPress - $result = wp_insert_post($post_data); - if (is_wp_error($result)) { - // log_to_file("Error creating DNS Record Backup: " . $result->get_error_message()); - return false; - } - // log_to_file("DNS record backup created successfully"); - return true; - } /** - * Create a backup of all DNS records for a domain using Cloudflare's export endpoint. + * Perform a full BIND export for a domain and save a backup * - * @param mixed $domain The domain input (post object, array, or ID). - * @return int|WP_Error The post ID of the backup record or WP_Error on failure. + * @param mixed $domain The domain ID or name. + * @param array|null $record The specific DNS record to export, or null for all records. + * @return bool True on success, false on failure. */ - public static function create_dns_backup($domain) - { + public static function export_dns_zone($domain) { $domain_post = self::get_domain_post($domain); if (!$domain_post) { - throw new InvalidArgumentException('Invalid domain input.'); + throw new Exception(__('export_dns_zone - Invalid domain specified.', 'rl-mailwarmer')); } $domain_name = $domain_post->post_title; + // log_to_file("export_dns_zone - Attempting to export records for $domain_name"); + + // $credentials = self::get_cloudflare_credentials($domain_post); + $client = self::get_cloudflare_client($domain_post); + + $endpoint = "zones/{$client->getConfig('zone_id')}/dns_records/export"; + // if ($record) { + // $endpoint .= "?name=" . urlencode($record['name']) . "&type=" . urlencode($record['type']); + // } else { + // $endpoint .= "/export"; + // } try { - // Fetch Cloudflare credentials - $credentials = self::get_cloudflare_credentials($domain_post); - if (!$credentials) { - throw new RuntimeException('Cloudflare credentials not found for the domain.'); - } - - // Get the Cloudflare zone ID - $zone_id = self::get_cloudflare_zone_id($domain_name, $credentials); - if (!$zone_id) { - throw new RuntimeException("Zone ID not found for domain {$domain_name}."); - } - - // Fetch all DNS records using Cloudflare export API - $client = new \GuzzleHttp\Client(['timeout' => 10]); - $response = $client->get("https://api.cloudflare.com/client/v4/zones/{$zone_id}/dns_records/export", [ + $response = $client->get($endpoint, [ 'headers' => [ - 'Authorization' => "Bearer {$credentials['key']}", + 'Authorization' => "Bearer {$client->getConfig('api_key')}", 'Content-Type' => 'application/json', ], ]); if ($response->getStatusCode() !== 200) { - throw new RuntimeException('Failed to fetch DNS records from Cloudflare.'); + $data = json_decode($response->getBody(), true); + throw new \RuntimeException('Failed to export DNS records: ' . json_encode($data['errors'] ?? 'Unknown error')); } + + $data = (string) $response->getBody(); + // log_to_file("export_dns_zone - Exported data: ", $data); - $dns_records = $response->getBody()->getContents(); - - log_to_file("create_dns_backup - Exported DNS Records: $dns_records"); - - // Save the backup as a custom post - $backup_post = [ - 'post_type' => 'domain-dns-backup', - 'post_title' => "{$domain_name}-backup-" . current_time('mysql'), - 'post_status' => 'private', - 'meta_input' => [ - 'domain_id' => $domain_post->ID, - 'backup_data' => $dns_records, - ], - ]; - - $backup_post_id = wp_insert_post($backup_post); - if (is_wp_error($backup_post_id)) { - throw new RuntimeException('Failed to create DNS backup.'); + if ($data != '') { + $record['name'] = $domain_name; + $record['type'] = "FULL"; + $record['content'] = $data; + return RL_MailWarmer_DB_Helper::insert_dns_backup($domain, $record); + // return true; } - - return $backup_post_id; + // return 69; } catch (Exception $e) { - error_log('Error creating DNS backup: ' . $e->getMessage()); - return new WP_Error('dns_backup_error', $e->getMessage()); + error_log(__('Failed to export DNS records: ', 'rl-mailwarmer') . $e->getMessage()); } + log_to_file("export_dns_zone - Unable to export DNS record for $domain_name"); + + return false; } /** - * Get the Cloudflare zone ID for a domain. + * Generate a comprehensive email deliverability report for the domain. * - * @param string $domain The domain name. - * @param array $credentials The Cloudflare credentials (email and API key). - * @return string|null The Cloudflare zone ID or null if not found. + * @param mixed $domain The domain input (post object, array, or ID). + * @return array The generated report. */ - private static function get_cloudflare_zone_id($domain, $credentials) + public static function generate_domain_report($domain) { - $client = new \GuzzleHttp\Client(['timeout' => 10]); - $response = $client->get('https://api.cloudflare.com/client/v4/zones', [ - 'headers' => [ - 'Authorization' => "Bearer {$credentials['key']}", - 'Content-Type' => 'application/json', - ], - 'query' => ['name' => $domain], - ]); + $domain_post = self::get_domain_post($domain); + if (!$domain_post) { + throw new InvalidArgumentException('generate_domain_report - Invalid domain input.'); + } + $domain_name = $domain_post->post_title; - $zones = json_decode((string) $response->getBody(), true); - return $zones['result'][0]['id'] ?? null; + // log_to_file("generate_domain_report - Running generate_domain_report for " . $domain_name); + + $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), + ]; + + // log_to_file("generate_domain_report - Health Report for $domain_name: ", $report); + + // return false; + return $report; } + /** + * Save a domain health report as a custom post. + * + * @param mixed $domain The domain input (post object, array, or ID). + * @return int|WP_Error The post ID of the saved report or WP_Error on failure. + */ + public static function save_domain_health_report($domain) + { + $domain_post = self::get_domain_post($domain); + if (!$domain_post) { + throw new InvalidArgumentException('Invalid domain input.'); + } + // log_to_file("save_domain_health_report - Trying to generate report for {$domain_post->post_title}"); + // log_to_file("save_domain_health_report - Before Report Generation"); + $report = self::generate_domain_report($domain); + // log_to_file("save_domain_health_report - After Report Generation"); + // Prepare the post title and content + // $title = "{$domain_post->post_title}-" . current_time('timestamp'); + $content = json_encode($report); + // log_to_file("save_domain_health_report - Report JSON Content: ", $content); + $report_id = RL_MailWarmer_DB_Helper::insert_health_report_backup($domain, $content); + + if (is_wp_error($report_id)) { + wp_send_json_error($report_id->get_error_message()); + // return false; + } else { + update_post_meta($domain_post->ID,'domain_health_report',$content); + // wp_send_json_success($report_id); + return $report_id; + } + } /** * Check domain health by verifying WHOIS information and basic attributes. @@ -333,17 +385,13 @@ class RL_MailWarmer_Domain_Helper * @param mixed $domain The domain input (post object, array, or ID). * @return array Domain health details. */ - public static function check_domain_health($domain) + private static function check_domain_registration($domain_name) { - log_to_file("check_domain_health - Running check_domain_health for $domain"); - $domain_post = self::get_domain_post($domain); - if (!$domain_post) { - throw new InvalidArgumentException('Invalid domain input.'); - } + // log_to_file("check_domain_registration - Running check_domain_registration for $domain_name"); // Fetch WHOIS data - $whois_data = shell_exec("whois {$domain_post->post_title}"); + $whois_data = shell_exec("whois {$domain_name}"); if (!$whois_data) { return [ @@ -381,6 +429,170 @@ class RL_MailWarmer_Domain_Helper 'days_to_expiration' => $days_to_expiration, ]; } + + + /** + * Check A record for the domain. + * + * @param mixed $domain The domain input (post object, array, or ID). + * @return array A record details. + */ + private static function check_a_record($dns_records) + { + $domain_name = $dns_records[0]['zone_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') ) { + $ip = $record['content']; + $http_status = self::get_http_status($domain_name); + + return [ + 'ip' => $ip, + 'http_status' => $http_status, + 'https_enabled'=> $http_status === 200 && self::is_https_enabled($domain_name), + ]; + } + } + + return [ + 'ip' => false, + 'http_status' => false, + 'https_enabled'=> false, + ]; + } + + /** + * Get HTTP status code for the domain. + */ + private static function get_http_status($domain) + { + $client = new Client(['timeout' => 10]); + try { + $response = $client->get("http://{$domain}"); + return $response->getStatusCode(); + } catch (Exception $e) { + return null; + } + } + + /** + * Check if HTTPS is enabled for the domain. + */ + private static function is_https_enabled($domain) + { + $client = new Client(['timeout' => 10]); + try { + $response = $client->get("https://{$domain}"); + return $response->getStatusCode() === 200; + } catch (Exception $e) { + return false; + } + } + + + /** + * Check MX record for the domain. + * + * @param mixed $domain The domain input (post object, array, or ID). + * @return array MX record details. + */ + private static function check_mx_record($dns_records) + { + $domain_name = $dns_records[0]['zone_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') ) { + $host = $record['content']; + $ptr_record = gethostbyaddr(gethostbyname($host)); + + return [ + 'host' => $host, + 'ptr_valid' => $ptr_record !== false, + 'ptr_matches' => $ptr_record === $host, + ]; + } + } + + return [ + 'host' => false, + 'ptr_valid' => false, + 'ptr_matches' => false, + ]; + } + + /** + * Update or create an MX record for the specified domain in CloudFlare. + * + * @param mixed $domain The domain (can be ID, post object, or name). + * @param string $host The mail server host. + * @param int $priority The MX record priority. + * @return array|string Response from CloudFlare or error message. + */ + public static function update_mx_record($domain, $content, $priority, $ttl = 3600, $action = NULL) { + try { + // Normalize the domain and get CloudFlare credentials + $domain_post = self::get_domain_post($domain); + $client = self::get_cloudflare_client($domain); + $dns_records = self::fetch_dns_records($client); + $existing_record_id = null; + log_to_file("update_mx_record - DNS Records: ", $dns_records); + + if ( !empty($dns_records) && ($dns_records != [])) { + 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) { + if ($record['content'] === $content && $record['priority'] === $priority) { + $existing_record_id = $record['id']; + log_to_file("update_mx_record - Matching record found"); + break; + } + } + } + + // Prepare the payload for the MX record + // $payload = [ + // 'type' => 'MX', + // 'name' => $domain_post->post_title, + // 'content' => $content, + // 'priority' => $priority, + // 'ttl' => $ttl, // Default TTL: 1 hour + // ]; + + + // if ($existing_record_id) { + // // Update the existing MX record + // $response = $client->request('PUT', "zones/$zone_id/dns_records/$existing_record_id", [ + // 'json' => $payload, + // ]); + // } else { + // // Create a new MX record + // $response = $client->request('POST', "zones/$zone_id/dns_records", [ + // 'json' => $payload, + // ]); + // } + + 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); + log_to_file("update_mx_record - Result: ", $result); + + if (!$result['success']) { + throw new Exception('Failed to update or create the MX record in CloudFlare.'); + } + + return $result; // Return the CloudFlare response + } catch (Exception $e) { + log_to_file('update_mx_record - Error in update_mx_record: ' . $e->getMessage()); + return 'Error: ' . $e->getMessage(); + } + } + /** * Check SPF record for the domain. @@ -388,35 +600,28 @@ class RL_MailWarmer_Domain_Helper * @param mixed $domain The domain input (post object, array, or ID). * @return array SPF record details. */ - public static function check_spf_record($domain) + private static function check_spf_record($dns_records) { - log_to_file("check_spf_record - Running check_spf_record for $domain"); - $domain_post = self::get_domain_post($domain); - if (!$domain_post) { - throw new InvalidArgumentException('Invalid domain input.'); - } - - $credentials = self::get_cloudflare_credentials($domain_post); - if (!$credentials) { - throw new RuntimeException('Cloudflare credentials not found for the domain.'); - } - - $records = self::fetch_dns_records($domain_post->post_title, 'TXT', $credentials); - // log_to_file("All TXT records: ", $records); - foreach ($records as $record) { + $domain_name = $dns_records[0]['zone_name']; + // log_to_file("check_spf_record - Running check_spf_record for $domain_name"); + + foreach ($dns_records as $record) { // log_to_file("Found " . $record['name'] . " - ". $record['content']); - if ( ($record['name'] == $domain_post->post_title) && ( strpos($record['content'], 'v=spf1') !== false ) ) { + if ( ($record['name'] == $domain_name) && ( strpos($record['content'], 'v=spf1') !== false ) ) { // log_to_file("Match Found: " . $record['name'] . ": " . $record['content']); return [ - 'exists' => true, - 'content' => $record['content'], + 'content' => addslashes($record['content']), 'ttl' => $record['ttl'], 'all_mechanism' => strpos($record['content'], '-all') !== false ? '-all' : (strpos($record['content'], '~all') !== false ? '~all' : 'none'), ]; } } - return ['exists' => false]; + return [ + 'content' => false, + 'ttl' => false, + 'all_mechanism' => false, + ]; } /** @@ -431,21 +636,24 @@ class RL_MailWarmer_Domain_Helper */ public static function update_spf_record($domain, $host, $action, $all_policy = '-all', $ttl = 3600) { + // log_to_file("update_spf_record - Post ID: $domain $host $action $all_policy $ttl"); $domain_post = self::get_domain_post($domain); if (!$domain_post) { - throw new InvalidArgumentException('Invalid domain input.'); + throw new InvalidArgumentException('update_spf_record - Invalid domain input.'); } + $domain_name = $domain_post->post_title; + // log_to_file("update_spf_record - Post ID: $domain_name $host $action $all_policy $ttl"); - $credentials = self::get_cloudflare_credentials($domain_post); - if (!$credentials) { - throw new RuntimeException('Cloudflare credentials not found for the domain.'); - } + // log_to_file("update_spf_record - Updating SPF for $domain_name"); - $records = self::fetch_dns_records($domain_post->post_title, 'TXT', $credentials); + $client = self::get_cloudflare_client($domain); + // log_to_file("update_spf_record - Client created"); + $dns_records = self::fetch_dns_records($client, "TXT"); $spf_record = null; - foreach ($records as $record) { - if (($record['name'] === $domain_post->post_title) && strpos($record['content'], 'v=spf1') !== false) { + foreach ($dns_records as $record) { + // log_to_file("update_spf_record - Found record: ", $record); + if (($record['name'] === $domain_name) && strpos($record['content'], 'v=spf1') !== false) { $spf_record = $record; break; } @@ -461,8 +669,8 @@ class RL_MailWarmer_Domain_Helper // Ensure TXT content is wrapped in double quotes $wrapped_content = '"' . trim($new_content, '"') . '"'; - // log_to_file("No record found. Creating new record: $domain_post->post_title: $new_content"); - return self::update_dns_record($domain_post->ID, $domain_post->post_title, 'TXT', $domain_post->post_title, $wrapped_content, $ttl, $credentials); + // log_to_file("No record found. Creating new record: $domain_name: $wrapped_content"); + return self::update_dns_record($domain_post, $domain_name, 'TXT', $domain_name, $wrapped_content, $ttl); } // Update the existing SPF record @@ -484,7 +692,7 @@ class RL_MailWarmer_Domain_Helper // log_to_file("Existing record found. Updating"); // log_to_file("Old record: $domain_post->post_title: " .$record['content']); // log_to_file("New record: $domain_post->post_title: $updated_content"); - return self::update_dns_record($domain_post->ID, $domain_post->post_title, 'TXT', $domain_post->post_title, $updated_content, $ttl, $credentials); + return self::update_dns_record($domain_post->ID, $domain_post->post_title, 'TXT', $domain_post->post_title, $updated_content, $ttl); } @@ -495,25 +703,16 @@ class RL_MailWarmer_Domain_Helper * @param mixed $domain The domain input (post object, array, or ID). * @return array DMARC record details. */ - public static function check_dmarc_record($domain) + private static function check_dmarc_record($dns_records) { - log_to_file("check_dmarc_record - Running check_dmarc_record for $domain"); - $domain_post = self::get_domain_post($domain); - if (!$domain_post) { - throw new InvalidArgumentException('Invalid domain input.'); - } - - $credentials = self::get_cloudflare_credentials($domain_post); - if (!$credentials) { - throw new RuntimeException('Cloudflare credentials not found for the domain.'); - } - - $records = self::fetch_dns_records($domain_post->post_title, 'TXT', $credentials); - foreach ($records as $record) { - if ($record['name'] === "_dmarc.{$domain_post->post_title}") { + $domain_name = $dns_records[0]['zone_name']; + // log_to_file("check_dmarc_record - Running check_dmarc_record for $domain_name"); + + foreach ($dns_records as $record) { + if ($record['name'] === "_dmarc.{$domain_name}") { return [ 'exists' => true, - 'content' => $record['content'], + 'content' => addslashes($record['content']), 'ttl' => $record['ttl'], 'policy' => preg_match('/p=(\w+)/', $record['content'], $matches) ? $matches[1] : null, 'sp_policy' => preg_match('/sp=(\w+)/', $record['content'], $matches) ? $matches[1] : null, @@ -528,7 +727,20 @@ class RL_MailWarmer_Domain_Helper } } - return ['exists' => false]; + return [ + 'exists' => false, + 'content' => false, + 'ttl' => false, + 'policy' => false, + 'sp_policy' => false, + 'percentage' => false, + 'aggregate_rpt' => false, + 'forensic_rpt' => false, + 'report_interval' => false, + 'report_format' => false, + 'aspf' => false, + 'adkim' => false, + ]; } @@ -546,20 +758,20 @@ class RL_MailWarmer_Domain_Helper throw new InvalidArgumentException('Invalid domain input.'); } - $credentials = self::get_cloudflare_credentials($domain_post); - if (!$credentials) { - throw new RuntimeException('Cloudflare credentials not found for the domain.'); - } + $domain_name = $domain_post->post_title; - $name = "_dmarc.{$domain_post->post_title}"; + $client = self::get_cloudflare_client($domain); + + // Fetch existing DMARC record + $dns_records = self::fetch_dns_records($client, "TXT"); + + $name = "_dmarc.{$domain_name}"; // log_to_file("update_dmarc_record - Updating DMARC record for $name"); - // Fetch existing DMARC record - $records = self::fetch_dns_records($domain_post->post_title, 'TXT', $credentials); $existing_record = null; - foreach ($records as $record) { + foreach ($dns_records as $record) { if ($record['name'] === $name) { log_to_file("update_dmarc_record - Match found: " . $record['name']); $existing_record = $record; @@ -634,41 +846,35 @@ class RL_MailWarmer_Domain_Helper * @param array $selectors Array of DKIM selectors to check. * @return array DKIM record details for each selector. */ - public static function check_dkim_record($domain, $selectors = []) + private static function check_dkim_record($dns_records, $selectors = []) { - log_to_file("check_dkim_record - Running check_dkim_record for $domain"); - $domain_post = self::get_domain_post($domain); - if (!$domain_post) { - throw new InvalidArgumentException('Invalid domain input.'); - } - - $credentials = self::get_cloudflare_credentials($domain_post); - if (!$credentials) { - throw new RuntimeException('Cloudflare credentials not found for the domain.'); - } - - $dns_records = self::fetch_dns_records($domain_post->post_title, 'TXT', $credentials); - + $domain_name = $dns_records[0]['zone_name']; + // log_to_file("check_dkim_record - Running check_dkim_record for $domain_name"); + + $dkim_records = []; $records = []; + $select_suffix = "._domainkey.{$domain_name}"; $default_selectors = ['google', 'selector1', 'selector2', 'k1', 'k2', 'ctct1', 'ctct2', 'sm', 's1', - 's2', 'sig1', 'litesrv', 'zendesk1', 'zendesk2', 'mail', 'email', 'dkim', 'default', '202410']; + 's2', 'sig1', 'litesrv', 'zendesk1', 'zendesk2', 'mail', 'email', 'dkim', 'default', '202410', '202406']; $selectors = array_unique(array_merge($default_selectors, $selectors)); + $selectors = array_map(function ($item) use ($select_suffix) { + return $item . $select_suffix; + }, $selectors); - foreach ($selectors as $selector) { - $name = "{$selector}._domainkey.{$domain_post->post_title}"; - - foreach ($dns_records as $record) { - if ($record['name'] === $name) { - $records[$selector] = [ - 'exists' => true, - 'ttl' => $record['ttl'], - 'content'=> $record['content'], - ]; - break; - } + foreach ($dns_records as $record) { + // log_to_file("check_dkim_record - Found DKIM Record: {$record['name']}"); + if ( in_array($record['name'], $selectors) ) { + // log_to_file("check_dkim_record - Found {$record['name']}._domainkey"); + $records[$record['name']] = [ + 'ttl' => $record['ttl'], + // 'content'=> $record['content'], + ]; + // break; } } + // $dkim_records = [ "dkim_records" => $records ]; + return $records; } @@ -690,13 +896,14 @@ class RL_MailWarmer_Domain_Helper throw new InvalidArgumentException('Invalid domain input.'); } - $credentials = self::get_cloudflare_credentials($domain_post); - if (!$credentials) { - throw new RuntimeException('Cloudflare credentials not found for the domain.'); - } + $domain_name = $domain_post->post_title; - $name = "{$selector}._domainkey.{$domain_post->post_title}"; - $dns_records = self::fetch_dns_records($domain_post->post_title, 'TXT', $credentials); + $client = self::get_cloudflare_client($domain); + + // Fetch existing DMARC record + $dns_records = self::fetch_dns_records($client, "TXT"); + + $name = "{$selector}._domainkey.{$domain_name}"; // Ensure TXT content is wrapped in double quotes $wrapped_content = '"' . trim($value, '"') . '"'; @@ -712,137 +919,29 @@ class RL_MailWarmer_Domain_Helper if ($action === 'add') { // Update existing DKIM record - return self::update_dns_record($domain_post->ID, $domain_post->post_title, 'TXT', $name, $wrapped_content, $ttl, $credentials); + return self::update_dns_record($domain_post->ID, $domain_post->post_title, 'TXT', $name, $wrapped_content, $ttl); } } } if ($action === 'add') { // Create new DKIM record if not found - return self::update_dns_record($domain_post->ID, $domain_post->post_title, 'TXT', $name, $wrapped_content, $ttl, $credentials); + return self::update_dns_record($domain_post->ID, $domain_post->post_title, 'TXT', $name, $wrapped_content, $ttl); } return false; } - - - /** - * Check A record for the domain. - * - * @param mixed $domain The domain input (post object, array, or ID). - * @return array A record details. - */ - public static function check_a_record($domain) - { - log_to_file("check_a_record - Running check_a_record for $domain"); - - $domain_post = self::get_domain_post($domain); - if (!$domain_post) { - throw new InvalidArgumentException('Invalid domain input.'); - } - - $credentials = self::get_cloudflare_credentials($domain_post); - if (!$credentials) { - throw new RuntimeException('Cloudflare credentials not found for the domain.'); - } - - $dns_records = self::fetch_dns_records($domain_post->post_title, 'A', $credentials); - - if (!empty($dns_records)) { - $record = $dns_records[0]; - $ip = $record['content']; - $http_status = self::get_http_status($domain_post->post_title); - - return [ - 'exists' => true, - 'ip' => $ip, - 'http_status' => $http_status, - 'https_enabled'=> $http_status === 200 && self::is_https_enabled($domain_post->post_title), - ]; - } - - return ['exists' => false]; - } - - /** - * Get HTTP status code for the domain. - */ - private static function get_http_status($domain) - { - $client = new Client(['timeout' => 10]); - try { - $response = $client->get("http://{$domain}"); - return $response->getStatusCode(); - } catch (Exception $e) { - return null; - } - } - - /** - * Check if HTTPS is enabled for the domain. - */ - private static function is_https_enabled($domain) - { - $client = new Client(['timeout' => 10]); - try { - $response = $client->get("https://{$domain}"); - return $response->getStatusCode() === 200; - } catch (Exception $e) { - return false; - } - } - - - /** - * Check MX record for the domain. - * - * @param mixed $domain The domain input (post object, array, or ID). - * @return array MX record details. - */ - public static function check_mx_record($domain) - { - log_to_file("check_mx_record - Running check_mx_record for $domain"); - - $domain_post = self::get_domain_post($domain); - if (!$domain_post) { - throw new InvalidArgumentException('Invalid domain input.'); - } - - $credentials = self::get_cloudflare_credentials($domain_post); - if (!$credentials) { - throw new RuntimeException('Cloudflare credentials not found for the domain.'); - } - - $dns_records = self::fetch_dns_records($domain_post->post_title, 'MX', $credentials); - - if (!empty($dns_records)) { - $record = $dns_records[0]; - $host = $record['content']; - $ptr_record = gethostbyaddr(gethostbyname($host)); - - return [ - 'exists' => true, - 'host' => $host, - 'ptr_valid' => $ptr_record !== false, - 'ptr_matches' => $ptr_record === $host, - ]; - } - - return ['exists' => false]; - } - - /** * Check if the domain or its associated IP is on any blacklist. * * @param mixed $host The domain or IP address to check. * @return array Blacklist check results. */ - public static function check_blacklists($host) + private static function check_blacklists($host) { - log_to_file("check_blacklists - Running check_blacklists for $host"); + // log_to_file("check_blacklists - Running check_blacklists for $host"); $blacklist_servers = [ 'zen.spamhaus.org', @@ -865,118 +964,6 @@ class RL_MailWarmer_Domain_Helper ]; } - - /** - * Generate a comprehensive email deliverability report for the domain. - * - * @param mixed $domain The domain input (post object, array, or ID). - * @return array The generated report. - */ - public static function generate_domain_report($domain) - { - $domain_post = self::get_domain_post($domain); - if (!$domain_post) { - throw new InvalidArgumentException('Invalid domain input.'); - } - - log_to_file("generate_domain_report - Running generate_domain_report for " . $domain_post->ID); - - return [ - 'domain_health' => self::check_domain_health($domain), - 'a_record' => self::check_a_record($domain), - 'mx_record' => self::check_mx_record($domain), - 'spf_record' => self::check_spf_record($domain), - 'dkim_record' => self::check_dkim_record($domain), - 'dmarc_record' => self::check_dmarc_record($domain), - 'blacklists' => self::check_blacklists($domain_post->post_title), - ]; - } - - /** - * Save a domain health report as a custom post. - * - * @param mixed $domain The domain input (post object, array, or ID). - * @return int|WP_Error The post ID of the saved report or WP_Error on failure. - */ - public static function save_domain_health_report($domain) - { - log_to_file("save_domain_health_report - Trying to save report for $domain"); - $domain_post = self::get_domain_post($domain); - if (!$domain_post) { - throw new InvalidArgumentException('Invalid domain input.'); - } - - log_to_file("save_domain_health_report - Before Report Generation"); - $report = self::generate_domain_report($domain); - log_to_file("save_domain_health_report - After Report Generation"); - - // Prepare the post title and content - $title = "{$domain_post->post_title}-" . current_time('timestamp'); - $content = json_encode($report, JSON_PRETTY_PRINT); - - $meta_fields = [ - 'domain_id' => $domain_post->ID, - 'domain_name' => $domain_post->post_title, - 'domain_valid' => $report['domain_health']['registration_valid'], - 'domain_age' => $report['domain_health']['domain_age'], - 'domain_days_to_expiration' => $report['domain_health']['days_to_expiration'], - 'a_record_valid' => $report['a_record']['exists'], - 'a_record_resolves' => $report['a_record']['ip'] ?? null, - 'http_status' => $report['a_record']['http_status'], - 'https_enabled' => $report['a_record']['https_enabled'], - 'mx_record_valid' => $report['mx_record']['exists'], - 'mx_record_ptr_valid' => $report['mx_record']['ptr_valid'], - 'mx_record_ptr_match' => $report['mx_record']['ptr_matches'], - 'spf_record_exists' => $report['spf_record']['exists'], - 'spf_record_content' => $report['spf_record']['content'] ?? null, - 'spf_record_ttl' => $report['spf_record']['ttl'] ?? null, - 'spf_record_all_mechanism' => $report['spf_record']['all_mechanism'] ?? null, - 'dmarc_record_exists' => $report['dmarc_record']['exists'], - 'dmarc_policy' => $report['dmarc_record']['policy'] ?? null, - 'dmarc_sp_policy' => $report['dmarc_record']['sp_policy'] ?? null, - 'dmarc_percentage' => $report['dmarc_record']['percentage'] ?? null, - 'dmarc_aspf' => $report['dmarc_record']['aspf'] ?? null, - 'dmarc_adkim' => $report['dmarc_record']['adkim'] ?? null, - 'dmarc_aggregate_rpt' => $report['dmarc_record']['aggregate_rpt'] ?? null, - 'dmarc_forensic_rpt' => $report['dmarc_record']['forensic_rpt'] ?? null, - 'dmarc_report_format' => $report['dmarc_record']['rf'] ?? null, - 'dmarc_report_interval' => $report['dmarc_record']['ri'] ?? null, - 'dkim_records' => $report['dkim_record'], - ]; - - // Create the Domain Health Report post - $post_id = wp_insert_post([ - 'post_type' => 'domain-health-report', - 'post_title' => $title, - 'post_status' => 'publish', - 'post_content'=> $content, - 'meta_input' => $meta_fields, - ]); - - if (is_wp_error($post_id)) { - // wp_send_json_error($post_id->get_error_message()); - return false; - - log_to_file("save_domain_health_report - Error creating Domain Health Report: " . $post_id->get_error_message()); - } else { - log_to_file("Created Domain Health Report. Updating Domain metadata for {$domain_post->post_title}"); - $removeKeys = array('domain_id', 'domain_name'); - - foreach($removeKeys as $key) { - unset($meta_fields[$key]); - } - $data = array( - 'ID' => $domain_post->ID, - 'meta_input' => $meta_fields - ); - - wp_update_post( $data ); - } - - // wp_send_json_success($post_id); - return $post_id; - } - /** * Fix deliverability-related DNS issues for the given domain. * @@ -989,25 +976,25 @@ class RL_MailWarmer_Domain_Helper if (!$domain_post) { throw new InvalidArgumentException('Invalid domain input.'); } - - $results = []; $domain_name = $domain_post->post_title; - // Fetch credentials - $credentials = self::get_cloudflare_credentials($domain_post); - if (!$credentials) { - throw new RuntimeException('Cloudflare credentials not found for the domain.'); - } + // Create a full backup of the DNS zone before starting + // log_to_file("fix_deliverability_dns_issues - Exporting DNS zone for $domain_name"); - // Fetch current TXT records - try { - $current_txt_records = self::fetch_dns_records($domain_name, 'TXT', $credentials); - // log_to_file("fix_deliverability_dns_issues - Current TXT records for $domain_name: ", $current_txt_records); - // $results['txt_records'] = $current_txt_records; - } catch (Exception $e) { - $results['txt_records'] = 'Error fetching TXT records: ' . $e->getMessage(); - return $results; + $backup_id = self::export_dns_zone($domain_post); + if (is_wp_error($backup_id)) { + log_to_file("fix_deliverability_dns_issues - Error while exporting DNS zone. Quitting"); + wp_send_json_error($backup_id->get_error_message()); } + // log_to_file("fix_deliverability_dns_issues - Done exporting DNS zone for $domain_name"); + + $results = []; + + $client = self::get_cloudflare_client($domain); + + // Fetch existing DMARC record + // log_to_file("fix_deliverability_dns_issues - Fetching TXT records for $domain_name"); + $dns_records = self::fetch_dns_records($client, "TXT"); // Helper to find specific records $findRecord = function ($records, $contains, $search_name = false) { @@ -1027,9 +1014,10 @@ class RL_MailWarmer_Domain_Helper }; // SPF + // log_to_file("fix_deliverability_dns_issues - Fixing SPF record for $domain_name"); $default_spf_include = get_field('default_spf_include', 'option'); if ($default_spf_include) { - $spf_record = $findRecord($current_txt_records, 'v=spf1'); + $spf_record = $findRecord($dns_records, 'v=spf1'); if ($spf_record) { // log_to_file("fix_deliverability_dns_issues - Current SPF record: ",$spf_record); if (strpos($spf_record['content'], $default_spf_include) === false) { @@ -1057,6 +1045,7 @@ class RL_MailWarmer_Domain_Helper } // DKIM + // log_to_file("fix_deliverability_dns_issues - Fixing DKIM record for $domain_name"); $servers = get_field('servers', $domain_post->ID); if ($servers && is_array($servers)) { $results['dkim'] = []; @@ -1065,7 +1054,7 @@ class RL_MailWarmer_Domain_Helper $value = get_field('dkim_value', $server); // log_to_file("fix_deliverability_dns_issues - DKIM $selector: $value"); if ($selector && $value) { - $dkim_record = $findRecord($current_txt_records, "{$selector}._domainkey", true); + $dkim_record = $findRecord($dns_records, "{$selector}._domainkey", true); if (!$dkim_record) { try { $dkim_result = self::update_dkim_record($domain_post, $selector, 'add', $value); @@ -1087,7 +1076,8 @@ class RL_MailWarmer_Domain_Helper } // DMARC - $dmarc_record = $findRecord($current_txt_records, '_dmarc', true); + // log_to_file("fix_deliverability_dns_issues - Fixing DMARC record for $domain_name"); + $dmarc_record = $findRecord($dns_records, '_dmarc', true); if (!$dmarc_record) { // log_to_file("fix_deliverability_dns_issues - No existing DMARC record found. Creating one."); $dmarc_params = [ @@ -1110,10 +1100,24 @@ class RL_MailWarmer_Domain_Helper $results['dmarc'] = "Existing DMARC record found: {$dmarc_record['content']}"; } // $results['health_report'] = ''; + // Save a health report after fixing DNS issues try { - $report_id = self::save_domain_health_report($domain); + // log_to_file("fix_deliverability_dns_issues - Running save_domain_health_report() for {$domain_name}"); + // $report_id = self::save_domain_health_report($domain_post); + $report = self::generate_domain_report($domain); + $content = json_encode($report); + $report_id = RL_MailWarmer_DB_Helper::insert_health_report_backup($domain, $content); + + if (is_wp_error($report_id)) { + wp_send_json_error($report_id->get_error_message()); + // return false; + } else { + update_post_meta($domain_post->ID,'domain_health_report',$content); + // wp_send_json_success($report_id); + } $results['health_report'] = "Health report saved successfully with ID {$report_id}."; + // log_to_file("fix_deliverability_dns_issues - Health report saved successfully with ID {$report_id}"); } catch (Exception $e) { $results['health_report'] = 'Error saving health report: ' . $e->getMessage(); } @@ -1121,6 +1125,22 @@ class RL_MailWarmer_Domain_Helper return $results; } + /** + * Enable Resend for the given domain. + * + * @param mixed $domain The domain input (post object, array, or ID). + * @return array Results of the actions performed. + */ + public static function enabled_resend_for_domain($domain) + { + $domain_post = self::get_domain_post($domain); + if (!$domain_post) { + throw new InvalidArgumentException('Invalid domain input.'); + } + + } + + /* * TO DO! * @@ -1136,7 +1156,7 @@ class RL_MailWarmer_Domain_Helper * @param string $action The action to perform: 'create', 'update', or 'delete'. * @return bool|WP_Error True on success, WP_Error on failure. */ - public static function modify_domain_account_on_server($domain_id, $action) + private static function modify_domain_account_on_server($domain_id, $action) { // Validate domain post $domain = get_post($domain_id); @@ -1212,3 +1232,126 @@ class RL_MailWarmer_Domain_Helper } + +add_action('restrict_manage_posts', function ($post_type) { + if ($post_type === 'domain') { + ?> + + + __('No domains selected.', 'rl-mailwarmer')]); + } + + foreach ($domain_ids as $domain_id) { + // Run the save_domain_health_report function + $result = RL_MailWarmer_Domain_Helper::save_domain_health_report($domain_id); + + if (is_wp_error($result)) { + wp_send_json_error(['message' => $result->get_error_message()]); + } + } + + wp_send_json_success(['message' => __('Domain health check completed for selected domains.', 'rl-mailwarmer')]); +}); + + + +add_action('add_meta_boxes', 'rl_mailwarmer_add_generate_accounts_metabox'); +function rl_mailwarmer_add_generate_accounts_metabox() { + add_meta_box( + 'rl-generate-accounts', + __('Generate Accounts', 'rl-mailwarmer'), + 'rl_mailwarmer_generate_accounts_metabox_callback', + 'domain', + 'side', + 'default' + ); +} + +function rl_mailwarmer_generate_accounts_metabox_callback($post) { + // Nonce field for security + wp_nonce_field('rl_generate_accounts_nonce', 'rl_generate_accounts_nonce_field'); + + ?> +

+ + +

+ +
+ admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('rl_generate_accounts_nonce'), + ]); + } +} + +add_action('wp_ajax_rl_generate_random_accounts', 'rl_mailwarmer_generate_random_accounts_handler'); +function rl_mailwarmer_generate_random_accounts_handler() { + check_ajax_referer('rl_generate_accounts_nonce', 'security'); + + $post_id = intval($_POST['post_id']); + $qty = intval($_POST['qty']); + + if (!$post_id || !$qty || $qty < 1 || $qty > 50) { + wp_send_json_error(__('Invalid input.', 'rl-mailwarmer')); + } + + $accounts = RL_MailWarmer_Email_Helper::generate_random_accounts($post_id, $qty); + + if (is_array($accounts)) { + wp_send_json_success($accounts); + } else { + wp_send_json_error($accounts); + } +} diff --git a/includes/class-rl-mailwarmer-email-handler.php b/includes/class-rl-mailwarmer-email-handler.php index 9ceef04..20351ec 100644 --- a/includes/class-rl-mailwarmer-email-handler.php +++ b/includes/class-rl-mailwarmer-email-handler.php @@ -138,8 +138,8 @@ class RL_MailWarmer_Email_Handler { $start_hour = 8; // 8 AM $end_hour = 18; // 6 PM - $random_hour = rand($start_hour, $end_hour - 1); - $random_minute = rand(0, 59); + $random_hour = mt_rand($start_hour, $end_hour - 1); + $random_minute = mt_rand(0, 59); return strtotime(date('Y-m-d', $timestamp) . " {$random_hour}:{$random_minute}:00"); } diff --git a/includes/class-rl-mailwarmer-email-helper.php b/includes/class-rl-mailwarmer-email-helper.php deleted file mode 100644 index 0450668..0000000 --- a/includes/class-rl-mailwarmer-email-helper.php +++ /dev/null @@ -1,471 +0,0 @@ - 'create', - // 'email' => 'johndoe@example.com', - // 'metadata' => [ - // 'full_name' => 'John Doe', - // 'mail_password' => 'securepassword123', - // 'email_provider' => 123, // Post ID of the email provider - // 'smtp_server' => 'smtp.example.com', - // 'smtp_port' => 587, - // 'imap_server' => 'imap.example.com', - // 'imap_port' => 993, - // ], - // ]); - - // // Update an Email Account - // $result = RL_MailWarmer_Email_Helper::modify_email_account([ - // 'action' => 'update', - // 'post_id' => $post_id, - // 'metadata' => [ - // 'full_name' => 'Jane Doe', - // 'mail_password' => 'newsecurepassword123', - // 'smtp_status' => 'connected', - // ], - // ]); - - // // Delete an Email Account - // $result = RL_MailWarmer_Email_Helper::modify_email_account([ - // 'action' => 'delete', - // 'post_id' => $post_id, - // ]); - - - - - public static function modify_email_account(array $args) - { - // Validate required arguments - if (empty($args['action']) || !in_array($args['action'], ['create', 'update', 'delete'], true)) { - throw new InvalidArgumentException('Invalid or missing action.'); - } - - /* - * Add validation to only delete email-account posts - * - */ - - - $action = $args['action']; - $post_id = $args['post_id'] ?? null; - - // Handle delete action - if ($action === 'delete') { - if (!$post_id) { - throw new InvalidArgumentException('Post ID is required for deletion.'); - } - return wp_delete_post($post_id, true); - } - - // Validate fields for create/update - $post_data = [ - 'post_type' => 'email-account', - 'post_status' => 'publish', - ]; - - // For "create", ensure no existing post with the same title - if ($action === 'create') { - if (empty($args['email'])) { - throw new InvalidArgumentException('Email is required for creating a new account.'); - } - - $existing_post = get_page_by_title($args['email'], OBJECT, 'email-account'); - if ($existing_post) { - throw new RuntimeException('An email account with this title already exists.'); - } - - $post_data['post_title'] = $args['email']; - } elseif ($action === 'update') { - if (!$post_id) { - throw new InvalidArgumentException('Post ID is required for updates.'); - } - $post_data['ID'] = $post_id; - } - - // Assemble metadata - $meta_args = $args['metadata'] ?? []; - // Generate a random password if mail_password is not provided - if (empty($meta_args['mail_password'])) { - $meta_args['mail_password'] = bin2hex(random_bytes(8)); // 16-character password - } - $post_data['meta_input'] = array_map('sanitize_text_field', $meta_args); - - // Save or update the post - $post_id = wp_insert_post($post_data); - if (is_wp_error($post_id)) { - throw new RuntimeException('Failed to save email account post: ' . $post_id->get_error_message()); - } - - return $post_id; - } - - - /** - * Check mail login credentials for an email account. - * - * Validates IMAP/SMTP connection settings for the given email account. - * - * @param mixed $email_account The email-account post object or ID. - * @param string|null $protocol Optional. The protocol to validate ('IMAP' or 'SMTP'). Defaults to both. - * @return array|WP_Error Validation results for IMAP and/or SMTP or WP_Error on failure. - */ - public static function check_mail_login($email_account, $protocol = null) - { - // Get the post object - $post = is_numeric($email_account) ? get_post($email_account) : $email_account; - if (!$post || $post->post_type !== 'email-account') { - return new WP_Error('invalid_post', __('Invalid email account post.', 'rl-mailwarmer')); - } - - // Fetch email provider and override defaults with saved values - $email_provider_id = get_post_meta($post->ID, 'email_provider', true); - // log_to_file("check_mail_login - "); - - log_to_file("check_mail_login - Email Provider ID $email_provider_id"); - $defaults = $email_provider_id ? self::get_provider_defaults($email_provider_id) : []; - log_to_file("check_mail_login - Email Provider Defaults: ", $defaults); - - // Fetch saved settings - $saved_settings = [ - 'mail_password' => get_post_meta($post->ID, 'mail_password', true), - 'imap_password' => get_post_meta($post->ID, 'imap_password', true), - 'imap_server' => get_post_meta($post->ID, 'imap_server', true), - 'imap_port' => get_post_meta($post->ID, 'imap_port', true), - 'smtp_password' => get_post_meta($post->ID, 'smtp_password', true), - 'smtp_server' => get_post_meta($post->ID, 'smtp_server', true), - 'smtp_port' => get_post_meta($post->ID, 'smtp_port', true), - ]; - - // Merge saved settings with defaults - $settings = array_merge($defaults, array_filter($saved_settings)); - - log_to_file("check_mail_login - Using settings: ", $settings); - - $results = []; - - // Validate IMAP connection if required - if ($protocol === null || strtoupper($protocol) === 'IMAP') { - $imap_result = self::validate_imap_connection($post->post_title, $settings); - log_to_file("check_mail_login - IMAP Result for " . $post->post_title . ": ", $imap_result); - $results['IMAP'] = $imap_result ? __('IMAP connection successful.', 'rl-mailwarmer') : __('IMAP connection failed.', 'rl-mailwarmer'); - } - - // Validate SMTP connection if required - if ($protocol === null || strtoupper($protocol) === 'SMTP') { - $smtp_result = self::validate_smtp_connection($post->post_title, $settings); - log_to_file("check_mail_login - SMTP Result for " . $post->post_title . ": ", $imap_result); - $results['SMTP'] = $smtp_result ? __('SMTP connection successful.', 'rl-mailwarmer') : __('SMTP connection failed.', 'rl-mailwarmer'); - } - log_to_file("check_mail_login - Full Results for " . $post->post_title . ": ", $results); - - return $results; - } - - /** - * Fetch default settings for an email provider. - * - * @param int $email_provider_id The post ID of the email provider. - * @return array The default server settings. - */ - private static function get_provider_defaults($email_provider_id) - { - return [ - 'imap_server' => get_post_meta($email_provider_id, 'default_imap_server', true), - 'imap_port' => get_post_meta($email_provider_id, 'default_imap_port', true), - 'smtp_server' => get_post_meta($email_provider_id, 'default_smtp_server', true), - 'smtp_port' => get_post_meta($email_provider_id, 'default_smtp_port', true), - ]; - } - - /** - * Validate an IMAP connection for an email account. - * - * @param string $email The email address. - * @param array $settings The server settings (imap_server, imap_port, imap_password). - * @return bool True if the connection is successful, false otherwise. - */ - private static function validate_imap_connection($email, $settings) - { - if ( empty($settings['imap_server']) || empty($settings['imap_port']) ) { - return false; // Missing required settings - } - - if (!empty($settings['imap_password'])) { - $password = $settings['imap_password']; - } else { - $password = $settings['mail_password']; - } - - // Try connecting to the IMAP server - $imap_stream = @imap_open( - '{' . $settings['imap_server'] . ':' . $settings['imap_port'] . '/imap/ssl}', - $email, - $password - ); - - if ($imap_stream) { - imap_close($imap_stream); // Close connection if successful - return true; - } - - return false; // Connection failed - } - - - /** - * Validate an SMTP connection for an email account using Symfony Mailer. - * - * @param string $email The email address. - * @param array $settings The server settings (smtp_server, smtp_port, smtp_password). - * @return bool True if the connection is successful, false otherwise. - */ - private static function validate_smtp_connection($email, $settings) - { - if (empty($settings['smtp_server']) || empty($settings['smtp_port']) ) { - return false; // Missing required settings - } - - if (!empty($settings['smtp_password'])) { - $password = $settings['smtp_password']; - } else { - $password = $settings['mail_password']; - } - - $test_to_email = "ruben@redlotusaustin.com"; - - try { - // Create the SMTP transport - $transport = new Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport( - $settings['smtp_server'], - $settings['smtp_port'] - ); - - // Set authentication details - $transport->setUsername($email); - $transport->setPassword($password); - - // Create the mailer - $mailer = new Symfony\Component\Mailer\Mailer($transport); - - // Send a test email - $test_email = (new Symfony\Component\Mime\Email()) - ->from($email) - ->to($test_to_email) - ->subject('SMTP Connection for ' . $email) - ->text('This is a test email to verify SMTP connection for ' . $email); - - $mailer->send($test_email); - - return true; - } catch (Exception $e) { - error_log('SMTP validation failed: ' . $e->getMessage()); - return false; - } - } - - - /** - * Generate random email accounts for the specified domain. - * - * @param string $domain The domain name to use for the email accounts. - * @param int $qty The number of email accounts to generate. Defaults to 1. - * @return array List of generated names and email addresses. - * @throws Exception If name pools are empty or post creation fails. - */ - public static function generate_random_accounts($domain, $qty = 1) - { - // Fetch name pools from ACF options - $first_name_pool = get_field('valid_first_name_pool', 'option'); - $last_name_pool = get_field('valid_last_name_pool', 'option'); - - if (empty($first_name_pool) || empty($last_name_pool)) { - throw new Exception(__('Name pools are empty. Please configure them in the ACF options.', 'rl-mailwarmer')); - } - - $first_names = explode(',', $first_name_pool); // Assume comma-separated list - $last_names = explode(',', $last_name_pool); - - $generated_accounts = []; - - for ($i = 0; $i < $qty; $i++) { - // Generate a random name - $first_name = trim($first_names[array_rand($first_names)]); - $last_name = trim($last_names[array_rand($last_names)]); - $full_name = "{$first_name} {$last_name}"; - - // Generate a semi-random email address - $email_formats = [ - "{$first_name}{$last_name}", - "{$first_name}.{$last_name}", - substr($first_name, 0, 1) . ".{$last_name}", - "{$first_name}.l", - substr($first_name, 0, 1) . $last_name, - ]; - $email_local_part = strtolower($email_formats[array_rand($email_formats)]); - $email_address = "{$email_local_part}@{$domain}"; - - // Create the email-account post - // $post_id = RL_MailWarmer_Email_Helper::modify_email_account([ - // 'action' => 'create', - // 'email' => $email_address, - // 'metadata' => [ - // 'full_name' => $full_name, - // 'mail_password' => bin2hex(random_bytes(8)), // Generate a secure random password - // ], - // ]); - - // if (!$post_id) { - // throw new Exception(__('Failed to create email account post.', 'rl-mailwarmer')); - // } - - // Add to results - $generated_accounts[] = [ - 'full_name' => $full_name, - 'email_address' => $email_address, - ]; - } - - return $generated_accounts; - } - - - /** - * Modify an email account on a VirtualMin server. - * - * @param int $account_id The email-account post ID. - * @param string $action The action to perform: 'create', 'update', or 'delete'. - * @return bool|WP_Error True on success, WP_Error on failure. - */ - public static function modify_email_account_on_server($account_id, $action) - { - // Validate email-account post - $email_account = get_post($account_id); - if (!$email_account || $email_account->post_type !== 'email-account') { - return new WP_Error('invalid_account', __('Invalid email account.', 'rl-mailwarmer')); - } - - // Fetch associated server posts - $domain_id = get_post_meta($account_id, 'domain_id', true); - if (!$domain_id) { - return new WP_Error('missing_domain', __('No associated domain found.', 'rl-mailwarmer')); - } - - $server_ids = get_post_meta($domain_id, 'associated_servers', true); // Assuming this field holds server post IDs - if (empty($server_ids) || !is_array($server_ids)) { - return new WP_Error('missing_servers', __('No associated servers found.', 'rl-mailwarmer')); - } - - // Fetch email account details - $email_address = $email_account->post_title; - $password = get_post_meta($account_id, 'mail_password', true); - [$username, $domain] = explode('@', $email_address); - - // Iterate over servers and perform the action - foreach ($server_ids as $server_id) { - $server = get_post($server_id); - if (!$server || $server->post_type !== 'server') { - continue; // Skip invalid server posts - } - - $server_ip = get_post_meta($server_id, 'ip_address', true); - $server_port = get_post_meta($server_id, 'ssh_port', true); - $server_user = get_post_meta($server_id, 'username', true); - $server_password = get_post_meta($server_id, 'ssh_private_key', true); - // $server_password = get_post_meta($server_id, 'password', true); - - if (!$server_ip || !$server_user || !$server_password) { - return new WP_Error('missing_server_details', __('Missing server credentials.', 'rl-mailwarmer')); - } - - // Build VirtualMin command - $command = "virtualmin"; - if ($action === 'create') { - $command .= " create-user --domain $domain --user $username --pass $password"; - } elseif ($action === 'update') { - $command .= " modify-user --domain $domain --user $username --pass $password"; - } elseif ($action === 'delete') { - $command .= " delete-user --domain $domain --user $username"; - } else { - return new WP_Error('invalid_action', __('Invalid action specified.', 'rl-mailwarmer')); - } - - // Execute the command via SSH - // $ssh = new phpseclib\Net\SSH2($server_ip); - // $key = new phpseclib\Crypt\PublicKeyLoader::loadPrivateKey($server_password); // Adjust for SSH key or plain password - $ssh = new SSH2($server_ip, $server_port); - - if (!empty($server_password)) { - // Load the private key from the postmeta field - $key = PublicKeyLoader::loadPrivateKey($server_password); - } else { - // Fallback to password-based authentication - // $key = $server_password; - log_to_file("modify_email_account_on_server - Server $$server_id ssh_private_key empty"); - return new WP_Error('ssh_login_failed', __('No private key found!', 'rl-mailwarmer')); - } - - if (!$ssh->login($server_user, $key)) { - return new WP_Error('ssh_login_failed', __('Failed to log into the server.', 'rl-mailwarmer')); - } - - $output = $ssh->exec($command); - - if (strpos($output, 'failed') !== false) { - return new WP_Error('command_failed', __('Failed to execute VirtualMin command.', 'rl-mailwarmer')); - } - } - - return true; // Success - } - - - /** - * Send or reply to a conversation email. - * - * @param int $conversation_id The conversation post ID. - * @param int $account_id The email account post ID. - * @return bool True on success, false on failure. - */ - public static function send_conversation_mail(int $conversation_id, int $account_id) - { - // Implementation goes here - } -} diff --git a/includes/class-rl-mailwarmer-scheduler.php b/includes/class-rl-mailwarmer-scheduler.php index 50ba711..59efe3e 100644 --- a/includes/class-rl-mailwarmer-scheduler.php +++ b/includes/class-rl-mailwarmer-scheduler.php @@ -1,89 +1,94 @@ 60, // 60 seconds = 1 minute + 'display' => __('Every Minute', 'rl-mailwarmer'), + ]; + $schedules['every_five_minutes'] = [ + 'interval' => 300, + 'display' => __('Every 5 Minutes', 'rl-mailwarmer'), + ]; + } + return $schedules; + } + + /** + * Schedule the cron task if not already scheduled. + */ + public static function schedule_cron_jobs() { + + // Message handler + if (!wp_next_scheduled('rl_mailwarmer_process_messages')) { + wp_schedule_event(time(), 'every_minute', 'rl_mailwarmer_process_messages'); + } + + // Conversation handler + if (!wp_next_scheduled('rl_mailwarmer_process_upcoming_conversations')) { + wp_schedule_event(time(), 'every_minute', 'rl_mailwarmer_process_upcoming_conversations'); } } /** - * Clear WP-Cron jobs on deactivation. + * Clear the scheduled task. */ - public static function clear_cron_jobs() - { - $timestamp = wp_next_scheduled('rl_mailwarmer_send_emails'); + public static function clear_cron_jobs() { + + // Message handler + $timestamp = wp_next_scheduled('rl_mailwarmer_process_messages'); if ($timestamp) { - wp_unschedule_event($timestamp, 'rl_mailwarmer_send_emails'); + wp_unschedule_event($timestamp, 'rl_mailwarmer_process_messages'); + } + + // Conversation handler + $timestamp = wp_next_scheduled('rl_mailwarmer_process_upcoming_conversations'); + if ($timestamp) { + wp_unschedule_event($timestamp, 'rl_mailwarmer_process_upcoming_conversations'); } } /** - * Send scheduled emails for campaigns. + * Process pending messages by delegating to the Message Handler. */ - public static function send_scheduled_emails() - { - // Fetch campaigns with active schedules - $campaigns = get_posts([ - 'post_type' => 'campaign', - 'post_status' => 'publish', - 'meta_query' => [ - [ - 'key' => 'email_schedule', - 'compare' => 'EXISTS', - ], - ], - ]); + public static function process_pending_messages() { + // log_to_file("schedule_cron_jobs ====================== Running Cron to process messages ========================"); + // RL_MailWarmer_Message_Handler::process_pending_messages(); + } - if (!$campaigns) { - return; - } - - foreach ($campaigns as $campaign) { - // Get scheduled emails - $schedule = get_post_meta($campaign->ID, 'email_schedule', true); - - if (!is_array($schedule)) { - continue; - } - - foreach ($schedule as $email) { - // Check if the email is ready to be sent - $send_time = strtotime($email['send_time']); - if ($send_time > time()) { - continue; - } - - // Send the email - RL_MailWarmer_Email_Handler::send_email($email); - - // Mark as sent - $email['sent'] = true; - } - - // Update the schedule - update_post_meta($campaign->ID, 'email_schedule', $schedule); - } + /** + * Process pending conversations by delegating to the Message Handler. + */ + public static function process_upcoming_conversations() { + // log_to_file("schedule_cron_jobs ====================== Running Cron to process conversations ========================"); + // RL_MailWarmer_Message_Handler::process_upcoming_conversations(); } } diff --git a/includes/composer.json b/includes/composer.json deleted file mode 100644 index e22e372..0000000 --- a/includes/composer.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "require": { - "guzzlehttp/guzzle": "^7.9", - "symfony/mailer": "^7.2", - "symfony/http-client": "^7.2", - "phpseclib/phpseclib": "^3.0" - } -} diff --git a/includes/composer.lock b/includes/composer.lock deleted file mode 100644 index d0608e0..0000000 --- a/includes/composer.lock +++ /dev/null @@ -1,2007 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "66d40a08be9462f6349bfc464e6ea505", - "packages": [ - { - "name": "doctrine/deprecations", - "version": "1.1.3", - "source": { - "type": "git", - "url": "https://github.com/doctrine/deprecations.git", - "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", - "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9", - "phpstan/phpstan": "1.4.10 || 1.10.15", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "psalm/plugin-phpunit": "0.18.4", - "psr/log": "^1 || ^2 || ^3", - "vimeo/psalm": "4.30.0 || 5.12.0" - }, - "suggest": { - "psr/log": "Allows logging deprecations via PSR-3 logger implementation" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", - "homepage": "https://www.doctrine-project.org/", - "support": { - "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.3" - }, - "time": "2024-01-30T19:34:25+00:00" - }, - { - "name": "doctrine/lexer", - "version": "2.1.1", - "source": { - "type": "git", - "url": "https://github.com/doctrine/lexer.git", - "reference": "861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6", - "reference": "861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6", - "shasum": "" - }, - "require": { - "doctrine/deprecations": "^1.0", - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9 || ^12", - "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", - "psalm/plugin-phpunit": "^0.18.3", - "vimeo/psalm": "^4.11 || ^5.21" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\Lexer\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", - "homepage": "https://www.doctrine-project.org/projects/lexer.html", - "keywords": [ - "annotations", - "docblock", - "lexer", - "parser", - "php" - ], - "support": { - "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/2.1.1" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", - "type": "tidelift" - } - ], - "time": "2024-02-05T11:35:39+00:00" - }, - { - "name": "egulias/email-validator", - "version": "3.2.6", - "source": { - "type": "git", - "url": "https://github.com/egulias/EmailValidator.git", - "reference": "e5997fa97e8790cdae03a9cbd5e78e45e3c7bda7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/e5997fa97e8790cdae03a9cbd5e78e45e3c7bda7", - "reference": "e5997fa97e8790cdae03a9cbd5e78e45e3c7bda7", - "shasum": "" - }, - "require": { - "doctrine/lexer": "^1.2|^2", - "php": ">=7.2", - "symfony/polyfill-intl-idn": "^1.15" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.8|^9.3.3", - "vimeo/psalm": "^4" - }, - "suggest": { - "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Egulias\\EmailValidator\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Eduardo Gulias Davis" - } - ], - "description": "A library for validating emails against several RFCs", - "homepage": "https://github.com/egulias/EmailValidator", - "keywords": [ - "email", - "emailvalidation", - "emailvalidator", - "validation", - "validator" - ], - "support": { - "issues": "https://github.com/egulias/EmailValidator/issues", - "source": "https://github.com/egulias/EmailValidator/tree/3.2.6" - }, - "funding": [ - { - "url": "https://github.com/egulias", - "type": "github" - } - ], - "time": "2023-06-01T07:04:22+00:00" - }, - { - "name": "guzzlehttp/guzzle", - "version": "7.9.2", - "source": { - "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b", - "shasum": "" - }, - "require": { - "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.3", - "guzzlehttp/psr7": "^2.7.0", - "php": "^7.2.5 || ^8.0", - "psr/http-client": "^1.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" - }, - "provide": { - "psr/http-client-implementation": "1.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.2", - "ext-curl": "*", - "guzzle/client-integration-tests": "3.0.2", - "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.39 || ^9.6.20", - "psr/log": "^1.1 || ^2.0 || ^3.0" - }, - "suggest": { - "ext-curl": "Required for CURL handler support", - "ext-intl": "Required for Internationalized Domain Name (IDN) support", - "psr/log": "Required for using the Log middleware" - }, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - } - }, - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "GuzzleHttp\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Jeremy Lindblom", - "email": "jeremeamia@gmail.com", - "homepage": "https://github.com/jeremeamia" - }, - { - "name": "George Mponos", - "email": "gmponos@gmail.com", - "homepage": "https://github.com/gmponos" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://github.com/sagikazarmark" - }, - { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" - } - ], - "description": "Guzzle is a PHP HTTP client library", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "psr-18", - "psr-7", - "rest", - "web service" - ], - "support": { - "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.2" - }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", - "type": "tidelift" - } - ], - "time": "2024-07-24T11:22:20+00:00" - }, - { - "name": "guzzlehttp/promises", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/guzzle/promises.git", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", - "shasum": "" - }, - "require": { - "php": "^7.2.5 || ^8.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" - }, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Promise\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - }, - { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" - } - ], - "description": "Guzzle promises library", - "keywords": [ - "promise" - ], - "support": { - "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.4" - }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", - "type": "tidelift" - } - ], - "time": "2024-10-17T10:06:22+00:00" - }, - { - "name": "guzzlehttp/psr7", - "version": "2.7.0", - "source": { - "type": "git", - "url": "https://github.com/guzzle/psr7.git", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", - "shasum": "" - }, - "require": { - "php": "^7.2.5 || ^8.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.1 || ^2.0", - "ralouphie/getallheaders": "^3.0" - }, - "provide": { - "psr/http-factory-implementation": "1.0", - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" - }, - "suggest": { - "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" - }, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Psr7\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "George Mponos", - "email": "gmponos@gmail.com", - "homepage": "https://github.com/gmponos" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://github.com/sagikazarmark" - }, - { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://sagikazarmark.hu" - } - ], - "description": "PSR-7 message implementation that also provides common utility methods", - "keywords": [ - "http", - "message", - "psr-7", - "request", - "response", - "stream", - "uri", - "url" - ], - "support": { - "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.0" - }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", - "type": "tidelift" - } - ], - "time": "2024-07-18T11:15:46+00:00" - }, - { - "name": "paragonie/constant_time_encoding", - "version": "v3.0.0", - "source": { - "type": "git", - "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", - "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", - "shasum": "" - }, - "require": { - "php": "^8" - }, - "require-dev": { - "phpunit/phpunit": "^9", - "vimeo/psalm": "^4|^5" - }, - "type": "library", - "autoload": { - "psr-4": { - "ParagonIE\\ConstantTime\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com", - "homepage": "https://paragonie.com", - "role": "Maintainer" - }, - { - "name": "Steve 'Sc00bz' Thomas", - "email": "steve@tobtu.com", - "homepage": "https://www.tobtu.com", - "role": "Original Developer" - } - ], - "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", - "keywords": [ - "base16", - "base32", - "base32_decode", - "base32_encode", - "base64", - "base64_decode", - "base64_encode", - "bin2hex", - "encoding", - "hex", - "hex2bin", - "rfc4648" - ], - "support": { - "email": "info@paragonie.com", - "issues": "https://github.com/paragonie/constant_time_encoding/issues", - "source": "https://github.com/paragonie/constant_time_encoding" - }, - "time": "2024-05-08T12:36:18+00:00" - }, - { - "name": "paragonie/random_compat", - "version": "v9.99.100", - "source": { - "type": "git", - "url": "https://github.com/paragonie/random_compat.git", - "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", - "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", - "shasum": "" - }, - "require": { - "php": ">= 7" - }, - "require-dev": { - "phpunit/phpunit": "4.*|5.*", - "vimeo/psalm": "^1" - }, - "suggest": { - "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." - }, - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com", - "homepage": "https://paragonie.com" - } - ], - "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", - "keywords": [ - "csprng", - "polyfill", - "pseudorandom", - "random" - ], - "support": { - "email": "info@paragonie.com", - "issues": "https://github.com/paragonie/random_compat/issues", - "source": "https://github.com/paragonie/random_compat" - }, - "time": "2020-10-15T08:29:30+00:00" - }, - { - "name": "phpseclib/phpseclib", - "version": "3.0.42", - "source": { - "type": "git", - "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/db92f1b1987b12b13f248fe76c3a52cadb67bb98", - "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98", - "shasum": "" - }, - "require": { - "paragonie/constant_time_encoding": "^1|^2|^3", - "paragonie/random_compat": "^1.4|^2.0|^9.99.99", - "php": ">=5.6.1" - }, - "require-dev": { - "phpunit/phpunit": "*" - }, - "suggest": { - "ext-dom": "Install the DOM extension to load XML formatted public keys.", - "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", - "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", - "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", - "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." - }, - "type": "library", - "autoload": { - "files": [ - "phpseclib/bootstrap.php" - ], - "psr-4": { - "phpseclib3\\": "phpseclib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jim Wigginton", - "email": "terrafrost@php.net", - "role": "Lead Developer" - }, - { - "name": "Patrick Monnerat", - "email": "pm@datasphere.ch", - "role": "Developer" - }, - { - "name": "Andreas Fischer", - "email": "bantu@phpbb.com", - "role": "Developer" - }, - { - "name": "Hans-Jürgen Petrich", - "email": "petrich@tronic-media.com", - "role": "Developer" - }, - { - "name": "Graham Campbell", - "email": "graham@alt-three.com", - "role": "Developer" - } - ], - "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", - "homepage": "http://phpseclib.sourceforge.net", - "keywords": [ - "BigInteger", - "aes", - "asn.1", - "asn1", - "blowfish", - "crypto", - "cryptography", - "encryption", - "rsa", - "security", - "sftp", - "signature", - "signing", - "ssh", - "twofish", - "x.509", - "x509" - ], - "support": { - "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.42" - }, - "funding": [ - { - "url": "https://github.com/terrafrost", - "type": "github" - }, - { - "url": "https://www.patreon.com/phpseclib", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", - "type": "tidelift" - } - ], - "time": "2024-09-16T03:06:04+00:00" - }, - { - "name": "psr/container", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" - }, - "time": "2021-11-05T16:47:00+00:00" - }, - { - "name": "psr/event-dispatcher", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/event-dispatcher.git", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", - "shasum": "" - }, - "require": { - "php": ">=7.2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\EventDispatcher\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Standard interfaces for event handling.", - "keywords": [ - "events", - "psr", - "psr-14" - ], - "support": { - "issues": "https://github.com/php-fig/event-dispatcher/issues", - "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" - }, - "time": "2019-01-08T18:20:26+00:00" - }, - { - "name": "psr/http-client", - "version": "1.0.3", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-client.git", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", - "shasum": "" - }, - "require": { - "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Client\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP clients", - "homepage": "https://github.com/php-fig/http-client", - "keywords": [ - "http", - "http-client", - "psr", - "psr-18" - ], - "support": { - "source": "https://github.com/php-fig/http-client" - }, - "time": "2023-09-23T14:17:50+00:00" - }, - { - "name": "psr/http-factory", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-factory.git", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "shasum": "" - }, - "require": { - "php": ">=7.1", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", - "keywords": [ - "factory", - "http", - "message", - "psr", - "psr-17", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-factory" - }, - "time": "2024-04-15T12:06:14+00:00" - }, - { - "name": "psr/http-message", - "version": "2.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" - }, - "time": "2023-04-04T09:54:51+00:00" - }, - { - "name": "psr/log", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "shasum": "" - }, - "require": { - "php": ">=8.0.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "support": { - "source": "https://github.com/php-fig/log/tree/3.0.2" - }, - "time": "2024-09-11T13:17:53+00:00" - }, - { - "name": "ralouphie/getallheaders", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/ralouphie/getallheaders.git", - "reference": "120b605dfeb996808c31b6477290a714d356e822" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", - "reference": "120b605dfeb996808c31b6477290a714d356e822", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.1", - "phpunit/phpunit": "^5 || ^6.5" - }, - "type": "library", - "autoload": { - "files": [ - "src/getallheaders.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ralph Khattar", - "email": "ralph.khattar@gmail.com" - } - ], - "description": "A polyfill for getallheaders.", - "support": { - "issues": "https://github.com/ralouphie/getallheaders/issues", - "source": "https://github.com/ralouphie/getallheaders/tree/develop" - }, - "time": "2019-03-08T08:55:37+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "v3.5.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:20:29+00:00" - }, - { - "name": "symfony/event-dispatcher", - "version": "v7.2.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/event-dispatcher-contracts": "^2.5|^3" - }, - "conflict": { - "symfony/dependency-injection": "<6.4", - "symfony/service-contracts": "<2.5" - }, - "provide": { - "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "2.0|3.0" - }, - "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.2.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:21:43+00:00" - }, - { - "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "psr/event-dispatcher": "^1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\EventDispatcher\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to dispatching event", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:20:29+00:00" - }, - { - "name": "symfony/http-client", - "version": "v7.2.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-client.git", - "reference": "955e43336aff03df1e8a8e17daefabb0127a313b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/955e43336aff03df1e8a8e17daefabb0127a313b", - "reference": "955e43336aff03df1e8a8e17daefabb0127a313b", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "~3.4.3|^3.5.1", - "symfony/service-contracts": "^2.5|^3" - }, - "conflict": { - "amphp/amp": "<2.5", - "php-http/discovery": "<1.15", - "symfony/http-foundation": "<6.4" - }, - "provide": { - "php-http/async-client-implementation": "*", - "php-http/client-implementation": "*", - "psr/http-client-implementation": "1.0", - "symfony/http-client-implementation": "3.0" - }, - "require-dev": { - "amphp/http-client": "^4.2.1|^5.0", - "amphp/http-tunnel": "^1.0|^2.0", - "amphp/socket": "^1.1", - "guzzlehttp/promises": "^1.4|^2.0", - "nyholm/psr7": "^1.0", - "php-http/httplug": "^1.0|^2.0", - "psr/http-client": "^1.0", - "symfony/amphp-http-client-meta": "^1.0|^2.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpClient\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", - "homepage": "https://symfony.com", - "keywords": [ - "http" - ], - "support": { - "source": "https://github.com/symfony/http-client/tree/v7.2.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-11-29T08:22:02+00:00" - }, - { - "name": "symfony/http-client-contracts", - "version": "v3.5.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "c2f3ad828596624ca39ea40f83617ef51ca8bbf9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/c2f3ad828596624ca39ea40f83617ef51ca8bbf9", - "reference": "c2f3ad828596624ca39ea40f83617ef51ca8bbf9", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\HttpClient\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to HTTP clients", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-11-25T12:02:18+00:00" - }, - { - "name": "symfony/mailer", - "version": "v7.2.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/mailer.git", - "reference": "e4d358702fb66e4c8a2af08e90e7271a62de39cc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/e4d358702fb66e4c8a2af08e90e7271a62de39cc", - "reference": "e4d358702fb66e4c8a2af08e90e7271a62de39cc", - "shasum": "" - }, - "require": { - "egulias/email-validator": "^2.1.10|^3|^4", - "php": ">=8.2", - "psr/event-dispatcher": "^1", - "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/mime": "^7.2", - "symfony/service-contracts": "^2.5|^3" - }, - "conflict": { - "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<6.4", - "symfony/messenger": "<6.4", - "symfony/mime": "<6.4", - "symfony/twig-bridge": "<6.4" - }, - "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/twig-bridge": "^6.4|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Mailer\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Helps sending emails", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/mailer/tree/v7.2.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-11-25T15:21:05+00:00" - }, - { - "name": "symfony/mime", - "version": "v7.2.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/mime.git", - "reference": "cc84a4b81f62158c3846ac7ff10f696aae2b524d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/cc84a4b81f62158c3846ac7ff10f696aae2b524d", - "reference": "cc84a4b81f62158c3846ac7ff10f696aae2b524d", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/polyfill-intl-idn": "^1.10", - "symfony/polyfill-mbstring": "^1.0" - }, - "conflict": { - "egulias/email-validator": "~3.0.0", - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", - "symfony/mailer": "<6.4", - "symfony/serializer": "<6.4.3|>7.0,<7.0.3" - }, - "require-dev": { - "egulias/email-validator": "^2.1.10|^3.1|^4", - "league/html-to-markdown": "^5.0", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/serializer": "^6.4.3|^7.0.3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Mime\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Allows manipulating MIME messages", - "homepage": "https://symfony.com", - "keywords": [ - "mime", - "mime-type" - ], - "support": { - "source": "https://github.com/symfony/mime/tree/v7.2.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-11-23T09:19:39+00:00" - }, - { - "name": "symfony/polyfill-intl-idn", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", - "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", - "shasum": "" - }, - "require": { - "php": ">=7.2", - "symfony/polyfill-intl-normalizer": "^1.10" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Idn\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Laurent Bassin", - "email": "laurent@bassin.info" - }, - { - "name": "Trevor Rowbotham", - "email": "trevor.rowbotham@pm.me" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "idn", - "intl", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/service-contracts", - "version": "v3.5.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "psr/container": "^1.1|^2.0", - "symfony/deprecation-contracts": "^2.5|^3" - }, - "conflict": { - "ext-psr": "<1.1|>=2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\Service\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to writing services", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:20:29+00:00" - } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": {}, - "prefer-stable": false, - "prefer-lowest": false, - "platform": {}, - "platform-dev": {}, - "plugin-api-version": "2.6.0" -} diff --git a/includes/rl-mailwarmer-ajax.php b/includes/rl-mailwarmer-ajax.php index ccd435c..04dd5ea 100644 --- a/includes/rl-mailwarmer-ajax.php +++ b/includes/rl-mailwarmer-ajax.php @@ -32,12 +32,52 @@ add_action('wp_ajax_rl_mailwarmer_check_domain_health', function () { } }); +add_action('wp_ajax_rl_mailwarmer_update_mx_record', function () { + // Verify nonce + if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'update_mx_record_nonce')) { + wp_send_json_error(__('Invalid nonce', 'rl-mailwarmer')); + } + + // log_to_file("wp_ajax_rl_mailwarmer_update_mx_record - Running"); + + // Get input values + $post_id = intval($_POST['post_id']); + $action = sanitize_text_field($_POST['action_type']); + $content = sanitize_text_field($_POST['content']); + $priority = intval(sanitize_text_field($_POST['priority'])); + $ttl = intval($_POST['ttl']); + + if (!$post_id || !$content || !$action) { + wp_send_json_error(__('Missing required fields', 'rl-mailwarmer')); + } + $domain = RL_MailWarmer_Domain_Helper::get_domain_post($post_id); + + // log_to_file("wp_ajax_rl_mailwarmer_update_mx_record - Post ID: $post_id $content $action $priority $ttl"); + + // Call the update_mx_record function + try { + log_to_file("wp_ajax_rl_mailwarmer_update_mx_record - Before"); + $result = RL_MailWarmer_Domain_Helper::update_mx_record($domain, $content, $priority, $ttl, $action); + log_to_file("wp_ajax_rl_mailwarmer_update_mx_record - After"); + + if ($result) { + wp_send_json_success(__('MX record updated successfully.', 'rl-mailwarmer')); + } else { + wp_send_json_error(__('Failed to update MX record.', 'rl-mailwarmer')); + } + } catch (Exception $e) { + wp_send_json_error($e->getMessage()); + } +}); + add_action('wp_ajax_rl_mailwarmer_update_spf_record', function () { // Verify nonce if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'update_spf_record_nonce')) { wp_send_json_error(__('Invalid nonce', 'rl-mailwarmer')); } + // log_to_file("wp_ajax_rl_mailwarmer_update_spf_record - Running"); + // Get input values $post_id = intval($_POST['post_id']); $host = sanitize_text_field($_POST['host']); @@ -48,10 +88,15 @@ add_action('wp_ajax_rl_mailwarmer_update_spf_record', function () { if (!$post_id || !$host || !$action) { wp_send_json_error(__('Missing required fields', 'rl-mailwarmer')); } + $domain = RL_MailWarmer_Domain_Helper::get_domain_post($post_id); + + // log_to_file("wp_ajax_rl_mailwarmer_update_spf_record - Post ID: $post_id $host $action $all_policy $ttl"); // Call the update_spf_record function try { - $result = RL_MailWarmer_Domain_Helper::update_spf_record($post_id, $host, $action, $all_policy, $ttl); + // log_to_file("wp_ajax_rl_mailwarmer_update_spf_record - Before"); + $result = RL_MailWarmer_Domain_Helper::update_spf_record($domain, $host, $action, $all_policy, $ttl); + // log_to_file("wp_ajax_rl_mailwarmer_update_spf_record - After"); if ($result) { wp_send_json_success(__('SPF record updated successfully.', 'rl-mailwarmer')); @@ -164,10 +209,10 @@ add_action('wp_ajax_rl_mailwarmer_create_dns_backup', function () { if (!$post_id) { wp_send_json_error(__('Invalid post ID', 'rl-mailwarmer')); } - + $domain = get_post($post_id); // Call the create_dns_backup function try { - $backup_id = RL_MailWarmer_Domain_Helper::create_dns_backup($post_id); + $backup_id = RL_MailWarmer_Domain_Helper::export_dns_zone($domain); if (is_wp_error($backup_id)) { wp_send_json_error($backup_id->get_error_message()); } diff --git a/includes/rl-mailwarmer-domain-admin.php b/includes/rl-mailwarmer-domain-admin.php index 692454b..e2c6430 100644 --- a/includes/rl-mailwarmer-domain-admin.php +++ b/includes/rl-mailwarmer-domain-admin.php @@ -3,14 +3,15 @@ add_action('admin_enqueue_scripts', function ($hook) { if ($hook === 'post.php' || $hook === 'post-new.php') { global $post; + + wp_enqueue_style( + 'rl-mailwarmer-admin-css', + RL_MAILWARMER_URL . '/css/admin-style.css', // Path to your CSS file + [], + '1.0.3' // Version number + ); if ($post->post_type === 'domain') { - wp_enqueue_style( - 'rl-mailwarmer-admin-css', - RL_MAILWARMER_URL . '/css/admin-style.css', // Path to your CSS file - [], - '1.0.0' // Version number - ); wp_enqueue_script('rl-mailwarmer-admin-script', RL_MAILWARMER_URL . '/js/admin-check-domain-health.js', ['jquery'], null, true); wp_localize_script('rl-mailwarmer-admin-script', 'rlMailWarmer', [ 'ajax_url' => admin_url('admin-ajax.php'), @@ -23,6 +24,12 @@ add_action('admin_enqueue_scripts', function ($hook) { 'nonce' => wp_create_nonce('update_spf_record_nonce'), 'post_id' => $post->ID ]); + wp_enqueue_script('rl-mailwarmer-mx-script', RL_MAILWARMER_URL . '/js/admin-update-mx.js', ['jquery'], null, true); + wp_localize_script('rl-mailwarmer-mx-script', 'rlMailWarmerMx', [ + 'ajax_url' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('update_mx_record_nonce'), + 'post_id' => $post->ID + ]); wp_enqueue_script('rl-mailwarmer-dmarc-script', RL_MAILWARMER_URL . '/js/admin-update-dmarc.js', ['jquery'], null, true); wp_localize_script('rl-mailwarmer-dmarc-script', 'rlMailWarmerDmarc', [ 'ajax_url' => admin_url('admin-ajax.php'), @@ -134,6 +141,14 @@ add_action('add_meta_boxes', function () { 'side', // Context 'default' // Priority ); + add_meta_box( + 'update_mx_record_box', // Meta box ID + __('Update MX Record', 'rl-mailwarmer'), // Title + 'rl_mailwarmer_render_update_mx_record_box', // Callback function + 'domain', // Post type + 'side', // Context + 'default' // Priority + ); add_meta_box( 'update_spf_record_box', // Meta box ID __('Update SPF Record', 'rl-mailwarmer'), // Title @@ -223,6 +238,51 @@ function rl_mailwarmer_render_fix_deliverability_dns_issues_box($post) +/** + * Render the fields for the "Update MX Record" meta box. + * + * @param WP_Post $post The current post object. + */ +function rl_mailwarmer_render_update_mx_record_box($post) +{ + // Add a nonce field for security + wp_nonce_field('update_mx_record_nonce', 'update_mx_record_nonce_field'); + + // Render the fields + ?> +

+
+ +

+ +

+
+ +

+

+
+ +

+

+
+ +

+

+ +

+
+ __('Age', 'rl-mailwarmer'), 'domain_days_to_expiration' => __('Days to Expiration', 'rl-mailwarmer'), 'a_record_valid' => __('A Record Valid', 'rl-mailwarmer'), - 'a_record_resolves' => __('A Record Resolves To', 'rl-mailwarmer'), + // 'a_record_resolves' => __('A Record Resolves To', 'rl-mailwarmer'), 'http_status' => __('HTTP Status', 'rl-mailwarmer'), 'https_enabled' => __('HTTPS Enabled', 'rl-mailwarmer'), - 'mx_record_valid' => __('MX Valid', 'rl-mailwarmer'), - 'mx_record_ptr_valid' => __('PTR Valid', 'rl-mailwarmer'), - 'mx_record_ptr_match' => __('PTR Matches', 'rl-mailwarmer'), - 'spf_record_exists' => __('SPF Exists', 'rl-mailwarmer'), - 'spf_record_content' => __('SPF Valid', 'rl-mailwarmer'), - 'spf_record_ttl' => __('SPF TTL', 'rl-mailwarmer'), - 'spf_record_all_mechanism' => __('SPF All Mechanism', 'rl-mailwarmer'), + 'mx_record_valid' => __('Valid MX Record', 'rl-mailwarmer'), + // 'mx_record_ptr_valid' => __('PTR Valid', 'rl-mailwarmer'), + // 'mx_record_ptr_match' => __('PTR Matches', 'rl-mailwarmer'), + // 'spf_record_exists' => __('SPF Exists', 'rl-mailwarmer'), + 'spf_record_content' => __('SPF Record', 'rl-mailwarmer'), + // 'spf_record_ttl' => __('SPF TTL', 'rl-mailwarmer'), + // 'spf_record_all_mechanism' => __('SPF All Mechanism', 'rl-mailwarmer'), 'dmarc_record_exists' => __('DMARC Exists', 'rl-mailwarmer'), 'dmarc_policy' => __('DMARC Policy', 'rl-mailwarmer'), - 'dmarc_sp_policy' => __('DMARC SP Policy', 'rl-mailwarmer'), - 'dmarc_percentage' => __('DMARC Percentage', 'rl-mailwarmer'), - 'dmarc_aspf' => __('DMARC ASPF', 'rl-mailwarmer'), - 'dmarc_adkim' => __('DMARC ADKIM', 'rl-mailwarmer'), - 'dmarc_aggregate_rpt' => __('DMARC Aggregate RPT', 'rl-mailwarmer'), - 'dmarc_forensic_rpt' => __('DMARC Forensic RPT', 'rl-mailwarmer'), - 'dmarc_report_format' => __('DMARC Report Format', 'rl-mailwarmer'), - 'dmarc_report_interval' => __('DMARC Report Interval', 'rl-mailwarmer'), + // 'dmarc_sp_policy' => __('DMARC SP Policy', 'rl-mailwarmer'), + // 'dmarc_percentage' => __('DMARC Percentage', 'rl-mailwarmer'), + // 'dmarc_aspf' => __('DMARC ASPF', 'rl-mailwarmer'), + // 'dmarc_adkim' => __('DMARC ADKIM', 'rl-mailwarmer'), + // 'dmarc_aggregate_rpt' => __('DMARC Aggregate RPT', 'rl-mailwarmer'), + // 'dmarc_forensic_rpt' => __('DMARC Forensic RPT', 'rl-mailwarmer'), + // 'dmarc_report_format' => __('DMARC Report Format', 'rl-mailwarmer'), + // 'dmarc_report_interval' => __('DMARC Report Interval', 'rl-mailwarmer'), 'dkim_records' => __('DKIM Records', 'rl-mailwarmer'), ]; @@ -555,6 +615,7 @@ add_filter('manage_edit-domain-health-report_sortable_columns', function ($colum $columns['domain_name'] = 'domain_name'; $columns['domain_valid'] = 'domain_valid'; $columns['domain_age'] = 'domain_age'; + $columns['mx_record_valid'] = 'mx_record_valid'; // Add more sortable columns as needed return $columns; }); @@ -572,30 +633,31 @@ add_filter('manage_domain_posts_columns', function ($columns) { // Add custom columns $custom_columns = [ + // 'domain_name' => __('Domain Name', 'rl-mailwarmer'), 'domain_valid' => __('Valid', 'rl-mailwarmer'), 'domain_age' => __('Age', 'rl-mailwarmer'), 'domain_days_to_expiration' => __('Days to Expiration', 'rl-mailwarmer'), 'a_record_valid' => __('A Record Valid', 'rl-mailwarmer'), - 'a_record_resolves' => __('A Record Resolves To', 'rl-mailwarmer'), + // 'a_record_resolves' => __('A Record Resolves To', 'rl-mailwarmer'), 'http_status' => __('HTTP Status', 'rl-mailwarmer'), 'https_enabled' => __('HTTPS Enabled', 'rl-mailwarmer'), - 'mx_record_valid' => __('MX Valid', 'rl-mailwarmer'), - 'mx_record_ptr_valid' => __('PTR Valid', 'rl-mailwarmer'), - 'mx_record_ptr_match' => __('PTR Matches', 'rl-mailwarmer'), - 'spf_record_exists' => __('SPF Exists', 'rl-mailwarmer'), - 'spf_record_is_valid' => __('SPF Valid', 'rl-mailwarmer'), - 'spf_record_ttl' => __('SPF TTL', 'rl-mailwarmer'), - 'spf_record_all_mechanism' => __('SPF All Mechanism', 'rl-mailwarmer'), + 'mx_record' => __('MX Record', 'rl-mailwarmer'), + // 'mx_record_ptr_valid' => __('PTR Valid', 'rl-mailwarmer'), + // 'mx_record_ptr_match' => __('PTR Matches', 'rl-mailwarmer'), + // 'spf_record_exists' => __('SPF Exists', 'rl-mailwarmer'), + 'spf_record_content' => __('SPF Record', 'rl-mailwarmer'), + // 'spf_record_ttl' => __('SPF TTL', 'rl-mailwarmer'), + // 'spf_record_all_mechanism' => __('SPF All Mechanism', 'rl-mailwarmer'), 'dmarc_record_exists' => __('DMARC Exists', 'rl-mailwarmer'), 'dmarc_policy' => __('DMARC Policy', 'rl-mailwarmer'), - 'dmarc_sp_policy' => __('DMARC SP Policy', 'rl-mailwarmer'), - 'dmarc_percentage' => __('DMARC Percentage', 'rl-mailwarmer'), - 'dmarc_aspf' => __('DMARC ASPF', 'rl-mailwarmer'), - 'dmarc_adkim' => __('DMARC ADKIM', 'rl-mailwarmer'), - 'dmarc_aggregate_rpt' => __('DMARC Aggregate RPT', 'rl-mailwarmer'), - 'dmarc_forensic_rpt' => __('DMARC Forensic RPT', 'rl-mailwarmer'), - 'dmarc_report_format' => __('DMARC Report Format', 'rl-mailwarmer'), - 'dmarc_report_interval' => __('DMARC Report Interval', 'rl-mailwarmer'), + // 'dmarc_sp_policy' => __('DMARC SP Policy', 'rl-mailwarmer'), + // 'dmarc_percentage' => __('DMARC Percentage', 'rl-mailwarmer'), + // 'dmarc_aspf' => __('DMARC ASPF', 'rl-mailwarmer'), + // 'dmarc_adkim' => __('DMARC ADKIM', 'rl-mailwarmer'), + // 'dmarc_aggregate_rpt' => __('DMARC Aggregate RPT', 'rl-mailwarmer'), + // 'dmarc_forensic_rpt' => __('DMARC Forensic RPT', 'rl-mailwarmer'), + // 'dmarc_report_format' => __('DMARC Report Format', 'rl-mailwarmer'), + // 'dmarc_report_interval' => __('DMARC Report Interval', 'rl-mailwarmer'), 'dkim_records' => __('DKIM Records', 'rl-mailwarmer'), ]; @@ -610,108 +672,151 @@ add_filter('manage_domain_posts_columns', function ($columns) { */ add_action('manage_domain_posts_custom_column', function ($column, $post_id) { $meta = get_post_meta($post_id); + $json_report = $meta['domain_health_report'][0]; + + // Decode the JSON into an associative array + $report = json_decode($json_report, true); + // var_dump($report); + + // // Check for JSON decoding errors + // if (json_last_error() !== JSON_ERROR_NONE) { + // log_to_file("rl_mailwarmer_render_domain_metadata_table - JSON decode error for post {$post->ID}: " . json_last_error_msg()); + // // return null; + // } + // log_to_file("manage_domain_posts_custom_column - Report array: ", $report["domain_health"]); + + // Assign metadata to the array using $post_meta + $metadata = [ + 'domain_valid' => $report["domain_health"]['registration_valid'] ?? '', + 'domain_age' => $report["domain_health"]['domain_age'] ?? '', + 'domain_days_to_expiration' => $report["domain_health"]['days_to_expiration'] ?? '', + 'a_record_ip' => $report["a_record"]['ip'] ?? '', + 'http_status' => $report["a_record"]['http_status'] ?? '', + 'https_enabled' => $report["a_record"]['https_enabled'] ?? '', + 'mx_record_host' => $report["mx_record"]['host'] ?? '', + 'mx_record_ptr_valid' => $report["mx_record"]['ptr_valid'] ?? '', + 'mx_record_ptr_match' => $report["mx_record"]['ptr_matches'] ?? '', + 'spf_record' => $report["spf_record"]['content'] ?? '', + 'spf_record_ttl' => $report["spf_record"]['ttl'] ?? '', + 'spf_record_all_mechanism' => $report["spf_record"]['all_mechanism'] ?? '', + 'dmarc_record_exists' => $report["dmarc_record"]['exists'] ?? '', + 'dmarc_record_content' => $report["dmarc_record"]['content'] ?? '', + 'dmarc_record_ttl' => $report["dmarc_record"]['ttl'] ?? '', + 'dmarc_policy' => $report["dmarc_record"]['policy'] ?? '', + 'dmarc_sp_policy' => $report["dmarc_record"]['sp_policy'] ?? '', + 'dmarc_percentage' => $report["dmarc_record"]['percentage'] ?? '', + 'dmarc_aspf' => $report["dmarc_record"]['aspf'] ?? '', + 'dmarc_adkim' => $report["dmarc_record"]['adkim'] ?? '', + 'dmarc_aggregate_rpt' => $report["dmarc_record"]['aggregate_rpt'] ?? '', + 'dmarc_forensic_rpt' => $report["dmarc_record"]['forensic_rpt'] ?? '', + 'dmarc_report_format' => $report["dmarc_record"]['report_format'] ?? '', + 'dmarc_report_interval' => $report["dmarc_record"]['report_interval'] ?? '', + 'dkim_records' => $report['dkim_records'] ?? '', + ]; + + // log_to_file("manage_domain_posts_custom_column - Health report: ", $domain_report); switch ($column) { case 'domain_valid': - echo !empty($meta['domain_valid'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); + echo !empty($report["domain_health"]['registration_valid']) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); break; case 'domain_age': - echo esc_html($meta['domain_age'][0] ?? ''); + echo !empty($report["domain_health"]['domain_age']) ? $report["domain_health"]['domain_age'] : __('0', 'rl-mailwarmer'); break; - case 'domain_days_to_expiration': - echo esc_html($meta['domain_days_to_expiration'][0] ?? ''); + case 'mx_record': + echo !empty($report["mx_record"]['host']) ? $report["mx_record"]['host'] : 'Unset'; break; - case 'a_record_valid': - echo !empty($meta['a_record_valid'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); - break; + // case 'domain_days_to_expiration': + // echo esc_html($meta['domain_days_to_expiration'][0] ?? ''); + // break; - case 'a_record_resolves': - echo esc_html($meta['a_record_resolves'][0] ?? ''); - break; + // case 'a_record_valid': + // echo !empty($meta['a_record_valid'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); + // break; - case 'http_status': - echo esc_html($meta['http_status'][0] ?? ''); - break; + // case 'a_record_resolves': + // echo esc_html($meta['a_record_resolves'][0] ?? ''); + // break; - case 'https_enabled': - echo !empty($meta['https_enabled'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); - break; + // case 'http_status': + // echo esc_html($meta['http_status'][0] ?? ''); + // break; - case 'mx_record_valid': - echo !empty($meta['mx_record_valid'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); - break; + // case 'https_enabled': + // echo !empty($meta['https_enabled'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); + // break; - case 'mx_record_ptr_valid': - echo !empty($meta['mx_record_ptr_valid'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); - break; + // case 'mx_record_ptr_valid': + // echo !empty($meta['mx_record_ptr_valid'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); + // break; - case 'mx_record_ptr_match': - echo !empty($meta['mx_record_ptr_match'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); - break; + // case 'mx_record_ptr_match': + // echo !empty($meta['mx_record_ptr_match'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); + // break; - case 'spf_record_exists': - echo !empty($meta['spf_record_exists'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); - break; + // case 'spf_record_exists': + // echo !empty($meta['spf_record_exists'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); + // break; - case 'spf_record_is_valid': - echo !empty($meta['spf_record_is_valid'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); - break; + // case 'spf_record_is_valid': + // echo !empty($meta['spf_record_is_valid'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); + // break; - case 'spf_record_ttl': - echo esc_html($meta['spf_record_ttl'][0] ?? ''); - break; + // case 'spf_record_ttl': + // echo esc_html($meta['spf_record_ttl'][0] ?? ''); + // break; - case 'spf_record_all_mechanism': - echo esc_html($meta['spf_record_all_mechanism'][0] ?? ''); - break; + // case 'spf_record_all_mechanism': + // echo esc_html($meta['spf_record_all_mechanism'][0] ?? ''); + // break; - case 'dmarc_record_exists': - echo !empty($meta['dmarc_record_exists'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); - break; + // case 'dmarc_record_exists': + // echo !empty($meta['dmarc_record_exists'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); + // break; - case 'dmarc_policy': - echo esc_html($meta['dmarc_policy'][0] ?? ''); - break; + // case 'dmarc_policy': + // echo esc_html($meta['dmarc_policy'][0] ?? ''); + // break; - case 'dmarc_sp_policy': - echo esc_html($meta['dmarc_sp_policy'][0] ?? ''); - break; + // case 'dmarc_sp_policy': + // echo esc_html($meta['dmarc_sp_policy'][0] ?? ''); + // break; - case 'dmarc_percentage': - echo esc_html($meta['dmarc_percentage'][0] ?? ''); - break; + // case 'dmarc_percentage': + // echo esc_html($meta['dmarc_percentage'][0] ?? ''); + // break; - case 'dmarc_aspf': - echo esc_html($meta['dmarc_aspf'][0] ?? ''); - break; + // case 'dmarc_aspf': + // echo esc_html($meta['dmarc_aspf'][0] ?? ''); + // break; - case 'dmarc_adkim': - echo esc_html($meta['dmarc_adkim'][0] ?? ''); - break; + // case 'dmarc_adkim': + // echo esc_html($meta['dmarc_adkim'][0] ?? ''); + // break; - case 'dmarc_aggregate_rpt': - echo esc_html($meta['dmarc_aggregate_rpt'][0] ?? ''); - break; + // case 'dmarc_aggregate_rpt': + // echo esc_html($meta['dmarc_aggregate_rpt'][0] ?? ''); + // break; - case 'dmarc_forensic_rpt': - echo esc_html($meta['dmarc_forensic_rpt'][0] ?? ''); - break; + // case 'dmarc_forensic_rpt': + // echo esc_html($meta['dmarc_forensic_rpt'][0] ?? ''); + // break; - case 'dmarc_report_format': - echo esc_html($meta['dmarc_report_format'][0] ?? ''); - break; + // case 'dmarc_report_format': + // echo esc_html($meta['dmarc_report_format'][0] ?? ''); + // break; - case 'dmarc_report_interval': - echo esc_html($meta['dmarc_report_interval'][0] ?? ''); - break; + // case 'dmarc_report_interval': + // echo esc_html($meta['dmarc_report_interval'][0] ?? ''); + // break; - case 'dkim_records': - echo esc_html($meta['dkim_records'][0] ?? ''); - break; + // case 'dkim_records': + // echo esc_html($meta['dkim_records'][0] ?? ''); + // break; default: echo ''; @@ -723,6 +828,7 @@ add_filter('manage_edit-domain_sortable_columns', function ($columns) { // $columns['domain_name'] = 'domain_name'; $columns['domain_valid'] = 'domain_valid'; $columns['domain_age'] = 'domain_age'; + $columns['mx_record'] = 'mx_record'; // Add more sortable columns as needed return $columns; }); @@ -748,35 +854,52 @@ add_action('add_meta_boxes', function () { */ function rl_mailwarmer_render_domain_metadata_table($post) { - // Fetch all metadata for the current post - $post_meta = get_post_meta($post->ID); + + // Fetch the domain health report from post meta + $json_report = get_post_meta($post->ID, 'domain_health_report', true); + + // Check if the meta field exists and is not empty + if (empty($json_report)) { + return null; // or throw an exception, depending on your error handling preference + } + + // Decode the JSON into an associative array + $report = json_decode($json_report, true); + + // Check for JSON decoding errors + if (json_last_error() !== JSON_ERROR_NONE) { + log_to_file("rl_mailwarmer_render_domain_metadata_table - JSON decode error for post {$post->ID}: " . json_last_error_msg()); + // return null; + } + // log_to_file("rl_mailwarmer_render_domain_metadata_table - Report array: ", $report); // Assign metadata to the array using $post_meta $metadata = [ - 'domain_valid' => $post_meta['domain_valid'][0] ?? '', - 'domain_age' => $post_meta['domain_age'][0] ?? '', - 'domain_days_to_expiration' => $post_meta['domain_days_to_expiration'][0] ?? '', - 'a_record_resolves' => $post_meta['a_record_resolves'][0] ?? '', - 'http_status' => $post_meta['http_status'][0] ?? '', - 'https_enabled' => $post_meta['https_enabled'][0] ?? '', - 'mx_record_valid' => $post_meta['mx_record_valid'][0] ?? '', - 'mx_record_ptr_valid' => $post_meta['mx_record_ptr_valid'][0] ?? '', - 'mx_record_ptr_match' => $post_meta['mx_record_ptr_match'][0] ?? '', - 'spf_record_exists' => $post_meta['spf_record_exists'][0] ?? '', - 'spf_record_is_valid' => $post_meta['spf_record_is_valid'][0] ?? '', - 'spf_record_ttl' => $post_meta['spf_record_ttl'][0] ?? '', - 'spf_record_all_mechanism' => $post_meta['spf_record_all_mechanism'][0] ?? '', - 'dmarc_record_exists' => $post_meta['dmarc_record_exists'][0] ?? '', - 'dmarc_policy' => $post_meta['dmarc_policy'][0] ?? '', - 'dmarc_sp_policy' => $post_meta['dmarc_sp_policy'][0] ?? '', - 'dmarc_percentage' => $post_meta['dmarc_percentage'][0] ?? '', - 'dmarc_aspf' => $post_meta['dmarc_aspf'][0] ?? '', - 'dmarc_adkim' => $post_meta['dmarc_adkim'][0] ?? '', - 'dmarc_aggregate_rpt' => $post_meta['dmarc_aggregate_rpt'][0] ?? '', - 'dmarc_forensic_rpt' => $post_meta['dmarc_forensic_rpt'][0] ?? '', - 'dmarc_report_format' => $post_meta['dmarc_report_format'][0] ?? '', - 'dmarc_report_interval' => $post_meta['dmarc_report_interval'][0] ?? '', - 'dkim_records' => $post_meta['dkim_records'][0] ?? '', + 'domain_valid' => $report["domain_health"]['registration_valid'] ?? '', + 'domain_age' => $report["domain_health"]['domain_age'] ?? '', + 'domain_days_to_expiration' => $report["domain_health"]['days_to_expiration'] ?? '', + 'a_record_ip' => $report["a_record"]['ip'] ?? '', + 'http_status' => $report["a_record"]['http_status'] ?? '', + 'https_enabled' => $report["a_record"]['https_enabled'] ?? '', + 'mx_record_host' => $report["mx_record"]['host'] ?? '', + 'mx_record_ptr_valid' => $report["mx_record"]['ptr_valid'] ?? '', + 'mx_record_ptr_match' => $report["mx_record"]['ptr_matches'] ?? '', + 'spf_record' => $report["spf_record"]['content'] ?? '', + 'spf_record_ttl' => $report["spf_record"]['ttl'] ?? '', + 'spf_record_all_mechanism' => $report["spf_record"]['all_mechanism'] ?? '', + 'dmarc_record_exists' => $report["dmarc_record"]['exists'] ?? '', + 'dmarc_record_content' => $report["dmarc_record"]['content'] ?? '', + 'dmarc_record_ttl' => $report["dmarc_record"]['ttl'] ?? '', + 'dmarc_policy' => $report["dmarc_record"]['policy'] ?? '', + 'dmarc_sp_policy' => $report["dmarc_record"]['sp_policy'] ?? '', + 'dmarc_percentage' => $report["dmarc_record"]['percentage'] ?? '', + 'dmarc_aspf' => $report["dmarc_record"]['aspf'] ?? '', + 'dmarc_adkim' => $report["dmarc_record"]['adkim'] ?? '', + 'dmarc_aggregate_rpt' => $report["dmarc_record"]['aggregate_rpt'] ?? '', + 'dmarc_forensic_rpt' => $report["dmarc_record"]['forensic_rpt'] ?? '', + 'dmarc_report_format' => $report["dmarc_record"]['report_format'] ?? '', + 'dmarc_report_interval' => $report["dmarc_record"]['report_interval'] ?? '', + 'dkim_records' => $report['dkim_records'] ?? '', ]; // Render the table diff --git a/includes/rl-mailwarmer-functions.php b/includes/rl-mailwarmer-functions.php index 516b235..44a0c7b 100644 --- a/includes/rl-mailwarmer-functions.php +++ b/includes/rl-mailwarmer-functions.php @@ -1,22 +1,28 @@ format("Y/m/d h:i:s"); // Convert arrays and objects to JSON format if (is_array($data) || is_object($data)) { $data = json_encode($data); - $message = $message . " " . $data; + $message = $message . "\r\n" . $data; } else if ($data) { $message = $message . " " . $data; } @@ -25,15 +31,227 @@ function log_to_file($message = false, $data = false){ } } +function rl_mailwarmer_enqueue_scripts() { + wp_enqueue_script('jquery'); + wp_enqueue_script('jquery-ui-core'); + wp_enqueue_script('jquery-ui-tabs'); + wp_enqueue_style('jquery-ui-css', 'https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/themes/base/jquery-ui.min.css'); + + wp_add_inline_script('jquery', ' + jQuery(document).ready(function($) { + + $(".notice-dismiss").on("click", function() { + $(this).closest(".notice").fadeOut(); + }); + + $(".advanced-toggle").click(function() { + $(".advanced-content").slideToggle(); + $(this).toggleClass("open"); + }); + + }); + '); + + wp_enqueue_script('rl-mailwarmer-public-script', RL_MAILWARMER_URL . '/js/public-check-domain-health.js', ['jquery'], null, true); + wp_localize_script('rl-mailwarmer-public-script', 'rlMailWarmer_public', [ + 'ajax_url' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('check_domain_health_nonce'), + 'post_id' => get_the_ID() + ]); +} +add_action('wp_enqueue_scripts', 'rl_mailwarmer_enqueue_scripts'); + +/** + * Disable specific plugins based on the WP_ENVIRONMENT_TYPE + */ + +function disable_plugins_based_on_environment() { + // log_to_file("disable_plugins_based_on_environment - Disabling plugins"); + // Ensure the environment type is defined + if ( ! defined( 'WP_ENVIRONMENT_TYPE' ) ) { + return; + } + + // Retrieve the environment type + $environment = WP_ENVIRONMENT_TYPE; + + // log_to_file("disable_plugins_based_on_environment - Disabling plugins for $environment environment"); + + // Define plugins to disable based on environment in wp-config.php + $disabled_plugins = defined( 'DISABLED_PLUGINS' ) ? DISABLED_PLUGINS : array(); + + // Only proceed if the array is properly defined and not empty + if ( ! is_array( $disabled_plugins ) || empty( $disabled_plugins ) ) { + return; + } + + // Get the currently active plugins + $active_plugins = get_option( 'active_plugins', array() ); + + foreach ( $disabled_plugins as $plugin_env => $plugins ) { + // Skip if current environment doesn't match + if ( $environment !== $plugin_env ) { + continue; + } + + // Loop through plugins to disable for this environment + foreach ( $plugins as $plugin ) { + $plugin_key = array_search( $plugin, $active_plugins, true ); + + // If the plugin is active, deactivate it + if ( false !== $plugin_key ) { + unset( $active_plugins[ $plugin_key ] ); + } + } + } + + // Update the active plugins option + update_option( 'active_plugins', $active_plugins ); +} +add_action( 'plugins_loaded', 'disable_plugins_based_on_environment', 1 ); + +/** + * getRandomWeightedElement() + * Utility function for getting random values with weighting. + * Pass in an associative array, such as array('A'=>5, 'B'=>45, 'C'=>50) + * An array like this means that "A" has a 5% chance of being selected, "B" 45%, and "C" 50%. + * The return value is the array key, A, B, or C in this case. Note that the values assigned + * do not have to be percentages. The values are simply relative to each other. If one value + * weight was 2, and the other weight of 1, the value with the weight of 2 has about a 66% + * chance of being selected. Also note that weights should be integers. + * + * @param array $weightedValues + */ +function getRandomWeightedElement(array $weightedValues) { + $rand = mt_rand(1, (int) array_sum($weightedValues)); + + foreach ($weightedValues as $key => $value) { + $rand -= $value; + if ($rand <= 0) { + return $key; + } + } +} + +/** + * Retrieve the content of a textarea meta field and return it as an array if it's a comma-separated list or has multiple lines. + * + * @param int $post_id The ID of the post. + * @param string $meta_key The meta key to retrieve. + * @return array|string The processed array or the original string if no transformation is needed. + */ +function rl_get_textarea_meta_as_array($post_id, $meta_key) { + if ($post_id ==='option') { + $content = get_option('options_' . $meta_key); + } else { + $content = get_post_meta($post_id, $meta_key, true); + + } + + // log_to_file("rl_get_textarea_meta_as_array - Content {$content}"); + if (empty($content)) { + // log_to_file("rl_get_textarea_meta_as_array - {$meta_key} field not found for {$post_id}"); + return []; // Return an empty array if the meta field is empty + } + + // Check if the content contains multiple lines or is comma-separated + if (strpos($content, "\n") !== false || strpos($content, ',') !== false) { + // Normalize line breaks and split the content into an array + $content_array = preg_split('/[\r\n,]+/', $content); + + // Remove empty entries and trim whitespace + return array_filter(array_map('trim', $content_array)); + } + + return $content; // Return the original content if no transformation is needed +} + +/** + * + * Unset default dashboard widgets + * + */ + +function remove_dashboard_widgets() { + global $wp_meta_boxes; + + unset($wp_meta_boxes['dashboard']['normal']['core']['dashboard_right_now']); + unset($wp_meta_boxes['dashboard']['normal']['core']['dashboard_activity']); + unset($wp_meta_boxes['dashboard']['side']['core']['dashboard_quick_press']); + unset($wp_meta_boxes['dashboard']['side']['core']['dashboard_primary']); + // unset($wp_meta_boxes['dashboard']['normal']['core']['dashboard_incoming_links']); + // unset($wp_meta_boxes['dashboard']['normal']['core']['dashboard_plugins']); + // unset($wp_meta_boxes['dashboard']['normal']['core']['dashboard_recent_drafts']); + // unset($wp_meta_boxes['dashboard']['normal']['core']['dashboard_recent_comments']); + // unset($wp_meta_boxes['dashboard']['side']['core']['dashboard_secondary']); + // unset($wp_meta_boxes['dashboard']['normal']['high']['rank_math_dashboard_widget']); +} + +add_action('wp_dashboard_setup', 'remove_dashboard_widgets' ); + +/** + * + * Reorder the admin menu. Leaves positions 1-8 available + * + */ + +add_action('admin_head', 'mf_edit_admin_menu'); +function mf_edit_admin_menu(){ + + global $menu; + + $menu[0] = $menu[2]; // Move the Dashboard to the top + unset($menu[2]); // Unset the Dashboard (from original position) + + // $menu[8] = $menu[4]; // Copy 'separator1' to a new menu location + unset($menu[4]); // Unset separator1 (from original position) + + ///$menu[9] = $menu[5]; // Copy 'Posts' to a new menu location + unset($menu[5]); // Unset Posts (from original position) + + ksort($menu); // Sort the menu + +} +/** + * Append Login In/Out link to menu with a redirect to this page + */ +add_filter( 'wp_nav_menu_primary-menu_items','rl_mailwarmer_loginout_menu_link', 10, 1 ); +function rl_mailwarmer_loginout_menu_link( $menu ) { + $referrer_url = $_SERVER['REQUEST_URI']; + // $redirect_url = "/membership-levels"; + $redirect_url = "/dashboard"; + // log_to_file("referrer_url: $referrer_url"); + // if ($referrer_url == '/membership-levels/') { + // $redirect_url = "/dashboard"; + // } + $loginout = wp_loginout( $redirect_url, false ); + $menu .= $loginout; + return $menu; +} + +// Create a shortcode to match +add_shortcode( 'mailferno_login_link', 'mailferno_show_login_link' ); +function mailferno_show_login_link( ) { + $referrer_url = $_SERVER['REQUEST_URI']; + // $redirect_url = "/membership-levels"; + $redirect_url = "/dashboard/?redirect_to=%2Fdashboard"; + // log_to_file("referrer_url: $referrer_url"); + // if ($referrer_url == '/membership-levels/') { + // $redirect_url = "/dashboard"; + // } + $login = 'Already have an account? Login here!'; + return $login; +} /** * Add a meta box for testing the SSH connection. */ + add_action('add_meta_boxes', function () { add_meta_box( 'test_ssh_connection_box', @@ -45,6 +263,7 @@ add_action('add_meta_boxes', function () { ); }); + /** * Render the SSH connection test meta box. * @@ -130,3 +349,223 @@ add_action('wp_ajax_rl_mailwarmer_test_ssh_connection', function () { }); + +/** + * Add sidebars to custom post types + * + */ + +function register_custom_sidebars() { + register_sidebar(array( + 'name' => 'Domain Sidebar', + 'id' => 'sidebar-domain', + 'description' => 'Sidebar for domain post type', + 'before_widget' => '
', + 'after_widget' => '
', + 'before_title' => '

', + 'after_title' => '

' + )); + + register_sidebar(array( + 'name' => 'Email Address Sidebar', + 'id' => 'sidebar-email', + 'description' => 'Sidebar for email-address post type', + 'before_widget' => '
', + 'after_widget' => '
', + 'before_title' => '

', + 'after_title' => '

' + )); + + register_sidebar(array( + 'name' => 'Campaign Sidebar', + 'id' => 'sidebar-campaign', + 'description' => 'Sidebar for campaign post type', + 'before_widget' => '
', + 'after_widget' => '
', + 'before_title' => '

', + 'after_title' => '

' + )); +} +add_action('widgets_init', 'register_custom_sidebars'); + +// add_filter( 'generate_sidebar_layout','rl_mailwarmer_custom_sidebar_layout' ); +function rl_mailwarmer_custom_sidebar_layout( $layout ) +{ + $post_type = get_post_type(); + // log_to_file("rl_mailwarmer_custom_sidebar_layout - Setting custom sidebar for $post_type post.", $layout); + switch ($post_type) { + case 'domain': + $layout = 'sidebar-domain'; + break; + case 'email-address': + $layout = 'sidebar-email'; + break; + case 'campaign': + $layout = 'sidebar-campaign'; + break; + + default: + // return $layout; + break; + } + // log_to_file("rl_mailwarmer_custom_sidebar_layout - Setting custom sidebar for $post_type post to: $layout"); + + + // Or else, set the regular layout + return $layout; + +} + + +/** + * PaidMemberships Pro Customizations + * + */ + + +/** + * Redirects members-only content to the Membership Levels page if a user is logged out or not a member. + * + */ + +add_action( 'template_redirect', 'rl_mailwarmer_redirect_require_membership_access' ); +function rl_mailwarmer_redirect_require_membership_access() { + if ( function_exists( 'pmpro_has_membership_access' ) && ! pmpro_has_membership_access() ) { + wp_redirect( pmpro_url( 'login' ) ); + exit; + } +} + + + +/** + * Add our user fields: cloudflare_api_email, cloudflare_api_key. + * This callback fires during the init action hook. + */ +function mailferno_pmpro_add_user_fields() { + // Don't break if PMPro is out of date or not loaded. + if ( ! function_exists( 'pmpro_add_user_field' ) ) { + return false; + } + + // Store our field settings in an array. + $fields = array(); + + /* + Settings for a company text field that is shown in the user profile. + The text field has a custom size and CSS class. It is required. + Only members with or checking out for levels 1 and 2 will see the field. + */ + $fields[] = new PMPro_Field( + 'cloudflare_api_email', // input name and user meta key + 'text', // type of field + array( + 'label' => 'CloudFlare API Email', // custom field label + // 'size' => 40, // input size + 'class' => 'company', // custom class + 'profile' => true, // show in user profile + // 'required' => true, // make this field required + // 'levels' => array(1,2), // require level 1 or 2 for this field + 'memberslistcsv' => true // include in CSV export + ) + ); + + /* + Settings for a referral code field. + All users can set this at checkout. + Only admins can see it on the user profile page. + */ + $fields[] = new PMPro_Field( + 'cloudflare_api_key', // input name and user meta key key + 'text', // type of field + array( + 'label' => 'CloudFlare API Key', // custom field label + 'profile' => true, // only show in profile for admins + 'memberslistcsv' => true // include in CSV export + ) + ); + + // Add a field group to put our fields into. + pmpro_add_field_group( 'CloudFlare API Settings' ); + + // Add all of our fields into that group. + foreach ( $fields as $field ) { + pmpro_add_user_field( + 'CloudFlare API Settings', // Which group to add to. + $field // PMPro_Field object + ); + } + + // That's it. See the PMPro User Fields docs for more information. +} +add_action( 'init', 'mailferno_pmpro_add_user_fields' ); + + +/** + * Allow users to delete posts that they own + * + */ + +// Handle front-end deletions +function handle_frontend_post_deletion() { + if ( + isset($_GET['action']) && $_GET['action'] === 'delete' && + isset($_GET['post']) && + isset($_GET['_wpnonce']) + ) { + $post_id = intval($_GET['post']); + + // Verify nonce + if (!wp_verify_nonce($_GET['_wpnonce'], 'delete-post_' . $post_id)) { + wp_die('Security check failed'); + } + + // Check permissions + if (!can_delete_post($post_id)) { + wp_die('You do not have permission to delete this item'); + } + + // Delete the post + wp_delete_post($post_id, true); + + // Redirect back + $redirect_to = isset($_GET['redirect_to']) ? urldecode($_GET['redirect_to']) : ''; + if (empty($redirect_to)) { + $redirect_to = remove_query_arg(array('action', 'post', '_wpnonce')); + } + + wp_redirect(add_query_arg('deleted', '1', $redirect_to)); + exit; + } +} +add_action('template_redirect', 'handle_frontend_post_deletion'); + +// Check if the user is the owner (for non-admins) +function can_delete_post($post_id) { + if (current_user_can('administrator')) { + return true; + } + + $current_user_id = get_current_user_id(); + $post_owner_id = get_post_meta($post_id, 'owner_id', true); + + return $current_user_id == $post_owner_id; +} + +// Single Domain View + +function get_connection_status($credentials) { + if ($credentials === false) return "Error"; + + $missing = []; + foreach (['api_email', 'api_key', 'zone_id'] as $key) { + if (empty($credentials[$key])) $missing[] = $key; + } + + return empty($missing) ? "Connected" : "Missing: " . implode(", ", $missing); +} + +function mask_api_key($key) { + if (empty($key)) return ""; + return str_repeat("*", strlen($key) - 6) . substr($key, -6); +} \ No newline at end of file diff --git a/js/admin-check-domain-health.js b/js/admin-check-domain-health.js index da473ce..9eddf64 100644 --- a/js/admin-check-domain-health.js +++ b/js/admin-check-domain-health.js @@ -1,4 +1,5 @@ jQuery(document).ready(function ($) { + // console.log("Loaded check-domain-health.js"); $('#check-domain-health-button').on('click', function (e) { e.preventDefault(); diff --git a/js/campaign-timeline-heatmap.js b/js/campaign-timeline-heatmap.js index 985f874..254f540 100644 --- a/js/campaign-timeline-heatmap.js +++ b/js/campaign-timeline-heatmap.js @@ -26,14 +26,17 @@ jQuery(document).ready(function ($) { data: { source: response.data, x: 'date', - y: d => +d['volume'], + y: d => +d['target_volume'], }, date: { - start: new Date('2024-12-4'), + start: new Date(2024, 12, 4), }, + cellSize: 15, range: 1, // Number of months to display domain: { type: 'year' }, subDomain: { type: 'day' }, // Granularity: days + legend: [25, 50, 150, 200, 250], + legendColors: ["#efefef", "steelblue"], legend: { show: true, label: 'Daily Volume', @@ -50,6 +53,7 @@ jQuery(document).ready(function ($) { scheme: 'YlOrRd', }, }, + // start: new Date(2024, 12, 06), verticalOrientation: true, itemSelector: '#campaign-timeline-heatmap', }); diff --git a/js/generate-timeline.js b/js/generate-timeline.js index 47b1274..40bc851 100644 --- a/js/generate-timeline.js +++ b/js/generate-timeline.js @@ -15,6 +15,7 @@ jQuery(document).ready(function ($) { security: rlMailWarmerGenerateTimeline.nonce, }, success: function (response) { + console.log(response); if (response.success) { const timeline = response.data; let output = '

Timeline Generated: