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'); } } /** * 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). * @return array The DNS records. * @throws Exception If fetching records fails. */ private static function fetch_dns_records($client, $type = null) { 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], ]); // log_to_file("fetch_dns_records - response: ", $response); $data = json_decode($response->getBody()->getContents(), true); 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()); } } /** * Update or create a DNS record using the Cloudflare API. * * @param int $domainID The ID of the parent domain post. * @param string $domain The domain name. * @param string $type The record type (e.g., TXT). * @param string $name The record name. * @param string $content The record content. * @param int $ttl The TTL value. * @return bool True on success. */ private static function update_dns_record($domain, $domain_name, $type, $name, $content, $ttl, $priority = 0) { // 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.'); } $client = self::get_cloudflare_client($domain); $dns_records = self::fetch_dns_records($client, $type); // Search existing records first, update it if found foreach ($dns_records as $record) { if (($record['name'] === $name) && ($record['type'] === $type)) { if ($content === $record['content']) { 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("update_dns_record - Match found! Creating backup before updating"); // Backup the existing record before updating $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("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("zones/{$client->getConfig('zone_id')}/dns_records/{$record['id']}", [ 'headers' => [ 'Authorization' => "Bearer {$client->getConfig('api_key')}", 'Content-Type' => 'application/json', ], 'json' => [ 'type' => $type, 'name' => $name, 'content' => $content, 'ttl' => $ttl, 'priority'=> (int) $priority, ], ]); return true; } else { 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 log_to_file("update_dns_record - Creating new record"); $response = $client->post("zones/{$client->getConfig('zone_id')}/dns_records", [ 'headers' => [ 'Authorization' => "Bearer {$client->getConfig('api_key')}", 'Content-Type' => 'application/json', ], 'json' => [ 'type' => $type, 'name' => $name, 'content' => $content, 'ttl' => (int) $ttl, 'priority'=> (int) $priority, ], ]); $result = json_decode($response->getBody(), true); log_to_file("update_dns_record - Result: ", $result); return true; } /** * Perform a full BIND export for a domain and save a backup * * @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 export_dns_zone($domain) { $domain_post = self::get_domain_post($domain); if (!$domain_post) { 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 { $response = $client->get($endpoint, [ 'headers' => [ 'Authorization' => "Bearer {$client->getConfig('api_key')}", 'Content-Type' => 'application/json', ], ]); if ($response->getStatusCode() !== 200) { $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); 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 69; } catch (Exception $e) { 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; } /** * 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('generate_domain_report - Invalid domain input.'); } $domain_name = $domain_post->post_title; // 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. * * @param mixed $domain The domain input (post object, array, or ID). * @return array Domain health details. */ private static function check_domain_registration($domain_name) { // log_to_file("check_domain_registration - Running check_domain_registration for $domain_name"); // Fetch WHOIS data $whois_data = shell_exec("whois {$domain_name}"); if (!$whois_data) { return [ 'registration_valid' => false, 'domain_age' => null, 'days_to_expiration' => null, ]; } // Parse WHOIS data $registration_valid = true; $creation_date = null; $expiration_date = null; if (preg_match('/Creation Date:\s*(.+)/i', $whois_data, $matches)) { $creation_date = new DateTime(trim($matches[1])); } if (preg_match('/Expiration Date:\s*(.+)/i', $whois_data, $matches)) { $expiration_date = new DateTime(trim($matches[1])); } else if (preg_match('/Expiry Date:\s*(.+)/i', $whois_data, $matches)) { $expiration_date = new DateTime(trim($matches[1])); } if (!$creation_date || !$expiration_date || $expiration_date < new DateTime()) { $registration_valid = false; } $domain_age = $creation_date ? (new DateTime())->diff($creation_date)->format('%y years, %m months') : null; $days_to_expiration = $expiration_date ? (new DateTime())->diff($expiration_date)->days : null; return [ 'registration_valid' => $registration_valid, 'domain_age' => $domain_age, '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. * * @param mixed $domain The domain input (post object, array, or ID). * @return array SPF record details. */ private static function check_spf_record($dns_records) { $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_name) && ( strpos($record['content'], 'v=spf1') !== false ) ) { // log_to_file("Match Found: " . $record['name'] . ": " . $record['content']); return [ 'content' => addslashes($record['content']), 'ttl' => $record['ttl'], 'all_mechanism' => strpos($record['content'], '-all') !== false ? '-all' : (strpos($record['content'], '~all') !== false ? '~all' : 'none'), ]; } } return [ 'content' => false, 'ttl' => false, 'all_mechanism' => false, ]; } /** * Update SPF record for the domain. * * @param mixed $domain The domain input (post object, array, or ID). * @param string $host The hostname or IP to add or remove. * @param string $action The action to perform: 'add' or 'delete'. * @param string $all_policy The `all` mechanism to set (default: '-all'). * @param int $ttl The TTL value (default: 3600). * @return bool True on success. */ 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('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"); // log_to_file("update_spf_record - Updating SPF for $domain_name"); $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 ($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; } } // Determine the correct format for the $host $host_format = filter_var($host, FILTER_VALIDATE_IP) ? "ip4:$host" : "include:$host"; if (!$spf_record) { // Create a new SPF record $new_content = "v=spf1 +a +mx $host_format $all_policy"; // Ensure TXT content is wrapped in double quotes $wrapped_content = '"' . trim($new_content, '"') . '"'; // 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 $content_parts = explode(' ', $spf_record['content']); if ($action === 'add') { if (!in_array($host_format, $content_parts, true)) { array_splice($content_parts, -1, 0, $host_format); // Insert before `all` mechanism } } elseif ($action === 'delete') { $content_parts = array_filter($content_parts, fn($part) => $part !== $host_format); } // Ensure the `all` mechanism is set correctly $content_parts = array_filter($content_parts, fn($part) => !preg_match('/~?all/', $part)); $content_parts[] = $all_policy .'"'; $updated_content = implode(' ', $content_parts); // 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); } /** * Check DMARC record for the domain. * * @param mixed $domain The domain input (post object, array, or ID). * @return array DMARC record details. */ private static function check_dmarc_record($dns_records) { $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' => 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, 'percentage' => preg_match('/pct=(\d+)/', $record['content'], $matches) ? $matches[1] : null, 'aggregate_rpt' => preg_match('/rua=mailto:([^;]+)/', $record['content'], $matches) ? $matches[1] : null, 'forensic_rpt' => preg_match('/ruf=mailto:([^;]+)/', $record['content'], $matches) ? $matches[1] : null, 'report_interval' => preg_match('/ri=(\d+)/', $record['content'], $matches) ? $matches[1] : null, 'report_format' => preg_match('/rf=(\w+)/', $record['content'], $matches) ? $matches[1] : null, 'aspf' => preg_match('/aspf=(\w+)/', $record['content'], $matches) ? $matches[1] : null, 'adkim' => preg_match('/adkim=(\w+)/', $record['content'], $matches) ? $matches[1] : null, ]; } } 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, ]; } /** * Update DMARC record for the domain. * * @param mixed $domain The domain input (post object, array, or ID). * @param array $params Array of DMARC record parameters to update. * @return bool True on success. */ public static function update_dmarc_record($domain, $params = []) { $domain_post = self::get_domain_post($domain); if (!$domain_post) { throw new InvalidArgumentException('Invalid domain input.'); } $domain_name = $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"); $existing_record = null; foreach ($dns_records as $record) { if ($record['name'] === $name) { log_to_file("update_dmarc_record - Match found: " . $record['name']); $existing_record = $record; break; } } // Default DMARC record if $params is empty if (empty($params)) { $params = [ 'v' => 'DMARC1', 'p' => 'quarantine', 'sp' => 'quarantine', 'pct' => 100, 'aspf' => 'r', 'adkim' => 'r', ]; } // Parse existing record if available $content_parts = []; if ($existing_record) { parse_str(str_replace('; ', '&', $existing_record['content']), $content_parts); } // Update or merge parameters $updated_params = array_merge($content_parts, $params); // Build the DMARC record string $dmarc_content = ''; foreach ($updated_params as $key => $value) { $dmarc_content .= "{$key}={$value}; "; } $dmarc_content = rtrim($dmarc_content, '; '); // Remove trailing semicolon and space // Ensure TXT content is wrapped in double quotes $wrapped_content = '"' . trim($dmarc_content, '"') . '"'; // log_to_file("update_dmarc_record - New DMARC value: $wrapped_content"); // $dmarc_content .= '"'; // Update or create the DNS record if ($existing_record) { return self::update_dns_record( $domain_post->ID, $domain_post->post_title, 'TXT', $name, $wrapped_content, $existing_record['ttl'], $credentials ); } else { return self::update_dns_record( $domain_post->ID, $domain_post->post_title, 'TXT', $name, $wrapped_content, 3600, // Default TTL $credentials ); } } /** * Check DKIM records for the domain. * * @param mixed $domain The domain input (post object, array, or ID). * @param array $selectors Array of DKIM selectors to check. * @return array DKIM record details for each selector. */ private static function check_dkim_record($dns_records, $selectors = []) { $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', '202406']; $selectors = array_unique(array_merge($default_selectors, $selectors)); $selectors = array_map(function ($item) use ($select_suffix) { return $item . $select_suffix; }, $selectors); 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; } /** * Update or create a DKIM record for the domain. * * @param mixed $domain The domain input (post object, array, or ID). * @param string $selector The DKIM selector to update. * @param string $action The action to perform: 'add' or 'delete'. * @param string $value The DKIM public key (required for 'add'). * @param int $ttl The TTL value (default: 3600). * @return bool True on success. */ public static function update_dkim_record($domain, $selector, $action, $value = '', $ttl = 3600) { $domain_post = self::get_domain_post($domain); if (!$domain_post) { throw new InvalidArgumentException('Invalid domain input.'); } $domain_name = $domain_post->post_title; $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, '"') . '"'; foreach ($dns_records as $record) { if ($record['name'] === $name) { if ($action === 'delete') { // Delete existing DKIM record log_to_file("update_dkim_record - delete_dns_record() is not implemented!"); throw new RuntimeException('delete_dns_record() is not implemented!'); // return self::delete_dns_record($domain_post->post_title, $record['id'], $credentials); } if ($action === 'add') { // Update existing DKIM record 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); } return 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. */ private static function check_blacklists($host) { // log_to_file("check_blacklists - Running check_blacklists for $host"); $blacklist_servers = [ 'zen.spamhaus.org', 'bl.spamcop.net', 'b.barracudacentral.org', ]; $results = []; $ip = filter_var($host, FILTER_VALIDATE_IP) ? $host : gethostbyname($host); foreach ($blacklist_servers as $server) { $query = implode('.', array_reverse(explode('.', $ip))) . '.' . $server; $listed = gethostbyname($query) !== $query; $results[$server] = $listed ? 'Listed' : 'Not Listed'; } return [ 'host' => $host, 'blacklists' => $results, ]; } /** * Fix deliverability-related DNS issues 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 fix_deliverability_dns_issues($domain) { $domain_post = self::get_domain_post($domain); if (!$domain_post) { throw new InvalidArgumentException('Invalid domain input.'); } $domain_name = $domain_post->post_title; // Create a full backup of the DNS zone before starting // log_to_file("fix_deliverability_dns_issues - Exporting DNS zone for $domain_name"); $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) { foreach ($records as $record) { if ($search_name) { if (strpos($record['name'], $contains) !== false) { return $record; } } else { if (strpos($record['content'], $contains) !== false) { return $record; } } } return null; }; // 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($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) { // Add default_spf_include to the SPF record // $updated_spf_content = rtrim(str_replace('-all', '', $spf_record['content'])) . " include:{$default_spf_include} -all"; try { self::update_spf_record($domain_post, $default_spf_include, 'add'); $results['spf'] = "Updated SPF to include $default_spf_include"; } catch (Exception $e) { $results['spf'] = 'Error: ' . $e->getMessage(); } } else { $results['spf'] = 'SPF record already includes default include.'; } } else { try { $results['spf'] = self::update_spf_record($domain_post, $default_spf_include, 'add'); } catch (Exception $e) { $results['spf'] = 'Error: ' . $e->getMessage(); } // $results['spf'] = 'No SPF record found for the domain.'; } } else { $results['spf'] = 'No default SPF include found in settings.'; } // 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'] = []; foreach ($servers as $server) { $selector = get_field('dkim_selector', $server); $value = get_field('dkim_value', $server); // log_to_file("fix_deliverability_dns_issues - DKIM $selector: $value"); if ($selector && $value) { $dkim_record = $findRecord($dns_records, "{$selector}._domainkey", true); if (!$dkim_record) { try { $dkim_result = self::update_dkim_record($domain_post, $selector, 'add', $value); $results['dkim'][$selector] = $dkim_result ? "DKIM record for selector '{$selector}' added successfully." : "Failed to add DKIM record for selector '{$selector}'."; } catch (Exception $e) { $results['dkim'][$selector] = 'Error: ' . $e->getMessage(); } } else { $results['dkim'][$selector] = "DKIM record for selector '{$selector}' already exists."; } } else { $results['dkim'][$server] = 'Missing DKIM selector or value for server.'; } } } else { $results['dkim'] = 'No servers selected for the domain.'; } // DMARC // 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 = [ 'v' => 'DMARC1', 'p' => 'quarantine', 'pct' => 100, 'aspf' => 'relaxed', 'adkim' => 'relaxed', ]; try { $dmarc_result = self::update_dmarc_record($domain_post, $dmarc_params); $results['dmarc'] = $dmarc_result ? 'Default DMARC record created successfully.' : 'Failed to create DMARC record.'; } catch (Exception $e) { $results['dmarc'] = 'Error: ' . $e->getMessage(); } } else { // log_to_file("fix_deliverability_dns_issues - Existing DMARC record found. Doing nothing."); $results['dmarc'] = "Existing DMARC record found: {$dmarc_record['content']}"; } // $results['health_report'] = ''; // Save a health report after fixing DNS issues try { // 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(); } 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! * */ // modify_domain_on_server - Creates/Updates/Deletes the specified domain on the specified server if it doesn't already exist (VirtualMin). Checks/Updates DNS config to include server IP in SPF, DKIM key, etc. /** * Modify a domain account on a VirtualMin server. * * Creates, updates, or deletes a virtual server for the specified domain. * * @param int $domain_id The domain post ID. * @param string $action The action to perform: 'create', 'update', or 'delete'. * @return bool|WP_Error True on success, WP_Error on failure. */ private static function modify_domain_account_on_server($domain_id, $action) { // Validate domain post $domain = get_post($domain_id); if (!$domain || $domain->post_type !== 'domain') { return new WP_Error('invalid_domain', __('Invalid domain post.', 'rl-mailwarmer')); } // Fetch associated server posts $server_ids = get_post_meta($domain_id, 'associated_servers', true); // Assume this holds server IDs if (empty($server_ids) || !is_array($server_ids)) { return new WP_Error('missing_servers', __('No associated servers found for the domain.', 'rl-mailwarmer')); } $domain_name = $domain->post_title; // 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_user = get_post_meta($server_id, 'username', true); $server_port = get_post_meta($server_id, 'ssh_port', true) ?: 22; $server_password = get_post_meta($server_id, 'ssh_private_key', true); if (!$server_ip || !$server_user || !$server_password) { return new WP_Error('missing_server_credentials', __('Missing server credentials.', 'rl-mailwarmer')); } // Build VirtualMin command $command = "virtualmin"; if ($action === 'create') { $command .= " create-domain --domain $domain_name --unix --dir --web --email"; } elseif ($action === 'update') { $command .= " modify-domain --domain $domain_name"; } elseif ($action === 'delete') { $command .= " delete-domain --domain $domain_name"; } else { return new WP_Error('invalid_action', __('Invalid action specified.', 'rl-mailwarmer')); } // Execute the command via SSH // $ssh = new phpseclib3\Net\SSH2($server_ip, $ssh_port); // $key = phpseclib3\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_domain_account_on_server - Server $$server_id ssh_private_key empty"); return new WP_Error('ssh_login_failed', __('No private key found!', 'rl-mailwarmer')); } if (!$ssh->login($server_user, $key)) { return new WP_Error('ssh_login_failed', __('Failed to log into the server.', 'rl-mailwarmer')); } $output = $ssh->exec($command); if (strpos($output, 'failed') !== false) { return new WP_Error('command_failed', __('Failed to execute VirtualMin command.', 'rl-mailwarmer')); } } return true; // Success } } add_action('restrict_manage_posts', function ($post_type) { if ($post_type === '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); } }