ID, 'cloudflare_api_email', true); $key = get_post_meta($domain_post->ID, 'cloudflare_api_key', true); return $email && $key ? ['email' => $email, 'key' => $key] : null; } /** * 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. */ private static function fetch_dns_records($domain, $type, $credentials) { log_to_file("fetch_dns_records - Fetching records for $domain"); $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 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. * * @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. * @param array $credentials Cloudflare credentials. * @return bool True on success. */ private static function update_dns_record($domain_id, $domain, $type, $name, $content, $ttl, $credentials) { // log_to_file("Updating $name record on $domain with: " . $content); $domain_post = self::get_domain_post($domain_id); 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); // Search existing records first, update it if found foreach ($records as $record) { if ($record['name'] === $name) { 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("Match found! Creating backup before updating"); // Backup the existing record before updating $backup_response = self::backup_dns_record($record, $domain_post->ID); // log_to_file("Backup response: $backup_response"); if ( $backup_response ){ // log_to_file("Updating $name record on $domain with: " . $content); // log_to_file("Current value: " . $record['content']); // Update existing record $client->put("{$endpoint}/{$zone_id}/dns_records/{$record['id']}", [ 'headers' => [ 'Authorization' => "Bearer {$credentials['key']}", 'Content-Type' => 'application/json', ], 'json' => [ 'type' => $type, 'name' => $name, 'content' => $content, 'ttl' => $ttl, ], ]); return true; } else { // log_to_file("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", [ 'headers' => [ 'Authorization' => "Bearer {$credentials['key']}", 'Content-Type' => 'application/json', ], 'json' => [ 'type' => $type, 'name' => $name, 'content' => $content, 'ttl' => $ttl, ], ]); 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) { // Define the backup storage method (e.g., custom table, file, or post type) // Example: Saving backups as posts in a custom post type $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. * * @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. */ public static function create_dns_backup($domain) { $domain_post = self::get_domain_post($domain); if (!$domain_post) { throw new InvalidArgumentException('Invalid domain input.'); } $domain_name = $domain_post->post_title; 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", [ 'headers' => [ 'Authorization' => "Bearer {$credentials['key']}", 'Content-Type' => 'application/json', ], ]); if ($response->getStatusCode() !== 200) { throw new RuntimeException('Failed to fetch DNS records from Cloudflare.'); } $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.'); } return $backup_post_id; } catch (Exception $e) { error_log('Error creating DNS backup: ' . $e->getMessage()); return new WP_Error('dns_backup_error', $e->getMessage()); } } /** * Get the Cloudflare zone ID for a 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. */ private static function get_cloudflare_zone_id($domain, $credentials) { $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], ]); $zones = json_decode((string) $response->getBody(), true); return $zones['result'][0]['id'] ?? null; } /** * 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. */ public static function check_domain_health($domain) { 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.'); } // Fetch WHOIS data $whois_data = shell_exec("whois {$domain_post->post_title}"); 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 SPF record for the domain. * * @param mixed $domain The domain input (post object, array, or ID). * @return array SPF record details. */ public static function check_spf_record($domain) { 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) { // log_to_file("Found " . $record['name'] . " - ". $record['content']); if ( ($record['name'] == $domain_post->post_title) && ( strpos($record['content'], 'v=spf1') !== false ) ) { // log_to_file("Match Found: " . $record['name'] . ": " . $record['content']); return [ 'exists' => true, 'content' => $record['content'], 'ttl' => $record['ttl'], 'all_mechanism' => strpos($record['content'], '-all') !== false ? '-all' : (strpos($record['content'], '~all') !== false ? '~all' : 'none'), ]; } } return ['exists' => 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) { $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); $spf_record = null; foreach ($records as $record) { if (($record['name'] === $domain_post->post_title) && 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_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); } // 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, $credentials); } /** * Check DMARC record for the domain. * * @param mixed $domain The domain input (post object, array, or ID). * @return array DMARC record details. */ public static function check_dmarc_record($domain) { 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}") { return [ 'exists' => true, 'content' => $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]; } /** * 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.'); } $credentials = self::get_cloudflare_credentials($domain_post); if (!$credentials) { throw new RuntimeException('Cloudflare credentials not found for the domain.'); } $name = "_dmarc.{$domain_post->post_title}"; // 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) { 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. */ public static function check_dkim_record($domain, $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); $records = []; $default_selectors = ['google', 'selector1', 'selector2', 'k1', 'k2', 'ctct1', 'ctct2', 'sm', 's1', 's2', 'sig1', 'litesrv', 'zendesk1', 'zendesk2', 'mail', 'email', 'dkim', 'default', '202410']; $selectors = array_unique(array_merge($default_selectors, $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; } } } 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.'); } $credentials = self::get_cloudflare_credentials($domain_post); if (!$credentials) { throw new RuntimeException('Cloudflare credentials not found for the domain.'); } $name = "{$selector}._domainkey.{$domain_post->post_title}"; $dns_records = self::fetch_dns_records($domain_post->post_title, 'TXT', $credentials); // 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, $credentials); } } } 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 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) { 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, ]; } /** * 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. * * @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.'); } $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.'); } // 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; } // 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 $default_spf_include = get_field('default_spf_include', 'option'); if ($default_spf_include) { $spf_record = $findRecord($current_txt_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 $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($current_txt_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 $dmarc_record = $findRecord($current_txt_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 { $report_id = self::save_domain_health_report($domain); $results['health_report'] = "Health report saved successfully with ID {$report_id}."; } catch (Exception $e) { $results['health_report'] = 'Error saving health report: ' . $e->getMessage(); } return $results; } }