commit cbe6834c03f2f04ab3c22552b4530a426ddcea15 Author: ruben Date: Tue Dec 3 20:05:06 2024 -0600 First Commit - Domain Tools ready diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..923d2fa --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/includes/vendor/ diff --git a/css/admin-style.css b/css/admin-style.css new file mode 100644 index 0000000..e9ab426 --- /dev/null +++ b/css/admin-style.css @@ -0,0 +1,43 @@ +/* Admin Meta Box Styles */ +#fix_deliverability_dns_issues_box, +#update_dkim_record_box, +#update_dmarc_record_box, +#update_spf_record_box { + background: #f9f9f9; + 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 { + font-size: 16px; + font-weight: bold; + margin-bottom: 10px; +} + +button.button-primary { + background-color: #007cba; + border-color: #007cba; + color: #fff; + text-shadow: none; +} + +button.button-primary:hover { + background-color: #005a9c; + border-color: #005a9c; + color: #fff; +} + +.meta-box-sortables input, .meta-box-sortables textarea, .meta-box-sortables select, .meta-box-sortables input[type=number] { + width: 100%; + max-width: 100%; +} + +table.rl_admin_meta_table { + width: 100%; + table-layout: fixed; +} \ No newline at end of file diff --git a/includes/class-rl-mailwarmer-domain-helper.php b/includes/class-rl-mailwarmer-domain-helper.php new file mode 100644 index 0000000..d0f7742 --- /dev/null +++ b/includes/class-rl-mailwarmer-domain-helper.php @@ -0,0 +1,1122 @@ +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; + } + + +} diff --git a/includes/class-rl-mailwarmer-email-handler.php b/includes/class-rl-mailwarmer-email-handler.php new file mode 100644 index 0000000..9ceef04 --- /dev/null +++ b/includes/class-rl-mailwarmer-email-handler.php @@ -0,0 +1,157 @@ +isSMTP(); + $phpmailer->Host = $email['smtp_host']; + $phpmailer->SMTPAuth = true; + $phpmailer->Username = $email['smtp_username']; + $phpmailer->Password = $email['smtp_password']; + $phpmailer->SMTPSecure = $email['smtp_encryption']; // SSL or TLS + $phpmailer->Port = $email['smtp_port']; + }); + + // Prepare the email + $to = $email['recipient']; + $subject = $email['subject']; + $body = $email['body']; + $headers = [ + 'From: ' . $email['sender_name'] . ' <' . $email['sender_email'] . '>', + 'Reply-To: ' . $email['reply_to'], + ]; + + // Send the email + return wp_mail($to, $subject, $body, $headers); + } + + /** + * Generate random email content. + * + * @param int $campaign_id The campaign ID. + * @param string $recipient The recipient email address. + * @return array The email content including subject and body. + */ + public static function generate_email_content(int $campaign_id, string $recipient): array + { + // Fetch predefined topics and names + $topics = explode("\n", get_field('default_topic_pool', 'option')); + $first_names = explode("\n", get_field('valid_first_name_pool', 'option')); + $last_names = explode("\n", get_field('valid_last_name_pool', 'option')); + + // Randomize participants and topics + $subject = self::random_element($topics); + $sender_name = self::random_element($first_names) . ' ' . self::random_element($last_names); + $body_content = "Hi {$recipient},\n\n" . + "I wanted to reach out to discuss {$subject}. Let's connect soon!\n\n" . + "Best,\n{$sender_name}"; + + return [ + 'subject' => $subject, + 'body' => $body_content, + ]; + } + + /** + * Generate email schedule based on warmup calculations. + * + * @param int $campaign_id The campaign ID. + * @return array The email schedule. + */ + public static function generate_email_schedule(int $campaign_id): array + { + $start_date = get_field('start_date', $campaign_id) ?: date('Ymd'); + $warmup_period = (int) get_field('warmup_period', $campaign_id); + $target_volume = (int) get_field('target_volume', $campaign_id); + $emails_per_day = $target_volume / ($warmup_period * 7); + + $schedule = []; + $current_date = strtotime($start_date); + + for ($week = 0; $week < $warmup_period; $week++) { + for ($day = 1; $day <= 7; $day++) { + if (self::is_weekend($current_date)) { + $current_date = strtotime('+1 day', $current_date); + continue; + } + + for ($email_count = 0; $email_count < $emails_per_day; $email_count++) { + $schedule[] = [ + 'send_time' => date('Y-m-d H:i:s', self::random_business_hour($current_date)), + 'sent' => false, + ]; + } + + $current_date = strtotime('+1 day', $current_date); + } + } + + return $schedule; + } + + /** + * Check if a timestamp falls on a weekend. + * + * @param int $timestamp The timestamp to check. + * @return bool True if it's a weekend, false otherwise. + */ + private static function is_weekend(int $timestamp): bool + { + $day = date('N', $timestamp); + return $day >= 6; + } + + /** + * Generate a random business hour timestamp for a given day. + * + * @param int $timestamp The day's timestamp. + * @return int The timestamp within business hours. + */ + private static function random_business_hour(int $timestamp): int + { + $start_hour = 8; // 8 AM + $end_hour = 18; // 6 PM + $random_hour = rand($start_hour, $end_hour - 1); + $random_minute = rand(0, 59); + + return strtotime(date('Y-m-d', $timestamp) . " {$random_hour}:{$random_minute}:00"); + } + + /** + * Get a random element from an array. + * + * @param array $array The array to pick from. + * @return mixed The random element. + */ + private static function random_element(array $array) + { + return $array[array_rand($array)]; + } +} diff --git a/includes/class-rl-mailwarmer-scheduler.php b/includes/class-rl-mailwarmer-scheduler.php new file mode 100644 index 0000000..50ba711 --- /dev/null +++ b/includes/class-rl-mailwarmer-scheduler.php @@ -0,0 +1,89 @@ + 'campaign', + 'post_status' => 'publish', + 'meta_query' => [ + [ + 'key' => 'email_schedule', + 'compare' => 'EXISTS', + ], + ], + ]); + + 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); + } + } +} diff --git a/includes/composer.json b/includes/composer.json new file mode 100644 index 0000000..4dce3f3 --- /dev/null +++ b/includes/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "guzzlehttp/guzzle": "^7.9" + } +} diff --git a/includes/composer.lock b/includes/composer.lock new file mode 100644 index 0000000..5f58efd --- /dev/null +++ b/includes/composer.lock @@ -0,0 +1,615 @@ +{ + "_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": "7827c548fdcc7e87cb0ae341dd2c6b1b", + "packages": [ + { + "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": "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": "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" + } + ], + "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 new file mode 100644 index 0000000..ba9663a --- /dev/null +++ b/includes/rl-mailwarmer-ajax.php @@ -0,0 +1,179 @@ +get_error_message()); + + // log_to_file("wp_ajax_rl_mailwarmer_check_domain_health - Error creating Domain Health Report: " . $report_post_id->get_error_message()); + } + + wp_send_json_success($report_post_id); + } catch (Exception $e) { + wp_send_json_error($e->getMessage()); + // log_to_file("wp_ajax_rl_mailwarmer_check_domain_health - EXCEPTION while creating Domain Health Report: " . $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')); + } + + // Get input values + $post_id = intval($_POST['post_id']); + $host = sanitize_text_field($_POST['host']); + $action = sanitize_text_field($_POST['action_type']); + $all_policy = sanitize_text_field($_POST['all_policy']); + $ttl = intval($_POST['ttl']); + + if (!$post_id || !$host || !$action) { + wp_send_json_error(__('Missing required fields', 'rl-mailwarmer')); + } + + // Call the update_spf_record function + try { + $result = RL_MailWarmer_Domain_Helper::update_spf_record($post_id, $host, $action, $all_policy, $ttl); + + if ($result) { + wp_send_json_success(__('SPF record updated successfully.', 'rl-mailwarmer')); + } else { + wp_send_json_error(__('Failed to update SPF record.', 'rl-mailwarmer')); + } + } catch (Exception $e) { + wp_send_json_error($e->getMessage()); + } +}); + +add_action('wp_ajax_rl_mailwarmer_update_dmarc_record', function () { + if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'update_dmarc_record_nonce')) { + wp_send_json_error(__('Invalid nonce', 'rl-mailwarmer')); + } + + $post_id = intval($_POST['post_id']); + if (!$post_id) { + wp_send_json_error(__('Invalid post ID', 'rl-mailwarmer')); + } + + $params = array_filter([ + 'p' => sanitize_text_field($_POST['policy']), + 'sp' => sanitize_text_field($_POST['sp']), + 'pct' => intval($_POST['pct']), + 'aspf' => sanitize_text_field($_POST['aspf']), + 'adkim' => sanitize_text_field($_POST['adkim']), + 'rua' => sanitize_email($_POST['rua']) ? 'mailto:' . sanitize_email($_POST['rua']) : null, + 'ruf' => sanitize_email($_POST['ruf']) ? 'mailto:' . sanitize_email($_POST['ruf']) : null, + 'fo' => sanitize_text_field($_POST['fo']), + 'rf' => sanitize_text_field($_POST['rf']), + 'ri' => intval($_POST['ri']), + ]); + + try { + $result = RL_MailWarmer_Domain_Helper::update_dmarc_record($post_id, $params); + + if ($result) { + wp_send_json_success(__('DMARC record updated successfully.', 'rl-mailwarmer')); + } else { + wp_send_json_error(__('Failed to update DMARC record.', 'rl-mailwarmer')); + } + } catch (Exception $e) { + wp_send_json_error($e->getMessage()); + } +}); + +add_action('wp_ajax_rl_mailwarmer_update_dkim_record', function () { + // Verify nonce + if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'update_dkim_record_nonce')) { + wp_send_json_error(__('Invalid nonce', 'rl-mailwarmer')); + } + + // Get input values + $post_id = intval($_POST['post_id']); + $selector = sanitize_text_field($_POST['selector']); + $action = sanitize_text_field($_POST['action_type']); + $value = sanitize_textarea_field($_POST['value']); + $ttl = intval($_POST['ttl']); + + if (!$post_id || !$selector || !$action) { + wp_send_json_error(__('Missing required fields', 'rl-mailwarmer')); + } + + // Call the update_dkim_record function + try { + $result = RL_MailWarmer_Domain_Helper::update_dkim_record($post_id, $selector, $action, $value, $ttl); + + if ($result) { + wp_send_json_success(__('DKIM record updated successfully.', 'rl-mailwarmer')); + } else { + wp_send_json_error(__('Failed to update DKIM record.', 'rl-mailwarmer')); + } + } catch (Exception $e) { + wp_send_json_error($e->getMessage()); + } +}); + + +add_action('wp_ajax_rl_mailwarmer_fix_dns_issues', function () { + // Verify nonce + if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'fix_deliverability_dns_issues_nonce')) { + wp_send_json_error(__('Invalid nonce', 'rl-mailwarmer')); + } + + // Get the domain post ID + $post_id = intval($_POST['post_id']); + if (!$post_id) { + wp_send_json_error(__('Invalid post ID', 'rl-mailwarmer')); + } + + // Call the fix_deliverability_dns_issues function + try { + $results = RL_MailWarmer_Domain_Helper::fix_deliverability_dns_issues($post_id); + + wp_send_json_success($results); + } catch (Exception $e) { + wp_send_json_error($e->getMessage()); + } +}); + +add_action('wp_ajax_rl_mailwarmer_create_dns_backup', function () { + // Verify nonce + if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'create_dns_backup_nonce')) { + wp_send_json_error(__('Invalid nonce', 'rl-mailwarmer')); + } + + // Get the domain post ID + $post_id = intval($_POST['post_id']); + if (!$post_id) { + wp_send_json_error(__('Invalid post ID', 'rl-mailwarmer')); + } + + // Call the create_dns_backup function + try { + $backup_id = RL_MailWarmer_Domain_Helper::create_dns_backup($post_id); + if (is_wp_error($backup_id)) { + wp_send_json_error($backup_id->get_error_message()); + } + + wp_send_json_success($backup_id); + } catch (Exception $e) { + wp_send_json_error($e->getMessage()); + } +}); diff --git a/includes/rl-mailwarmer-domain-admin.php b/includes/rl-mailwarmer-domain-admin.php new file mode 100644 index 0000000..26750cd --- /dev/null +++ b/includes/rl-mailwarmer-domain-admin.php @@ -0,0 +1,746 @@ +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'), + 'nonce' => wp_create_nonce('check_domain_health_nonce'), + 'post_id' => $post->ID + ]); + wp_enqueue_script('rl-mailwarmer-spf-script', RL_MAILWARMER_URL . '/js/admin-update-spf.js', ['jquery'], null, true); + wp_localize_script('rl-mailwarmer-spf-script', 'rlMailWarmerSpf', [ + 'ajax_url' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('update_spf_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'), + 'nonce' => wp_create_nonce('update_dmarc_record_nonce'), + 'post_id' => $post->ID + ]); + wp_enqueue_script('rl-mailwarmer-dkim-script', RL_MAILWARMER_URL . '/js/admin-update-dkim.js', ['jquery'], null, true); + wp_localize_script('rl-mailwarmer-dkim-script', 'rlMailWarmerDkim', [ + 'ajax_url' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('update_dkim_record_nonce'), + 'post_id' => $post->ID + ]); + wp_enqueue_script('rl-mailwarmer-dns-fix-script', RL_MAILWARMER_URL . '/js/admin-fix-dns.js', ['jquery'], null, true); + wp_localize_script('rl-mailwarmer-dns-fix-script', 'rlMailWarmerDnsFix', [ + 'ajax_url' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('fix_deliverability_dns_issues_nonce'), + 'post_id' => $post->ID + ]); + wp_enqueue_script('rl-mailwarmer-dns-backup', RL_MAILWARMER_URL . '/js/admin-dns-backup.js' , ['jquery'], null, true); + wp_localize_script('rl-mailwarmer-dns-backup', 'rlMailWarmerDnsBackup', [ + 'ajax_url' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('create_dns_backup_nonce'), + ]); + } + } +}); + +/** + * Add "Check Domain Health" button to the sidebar of the domain edit screen. + */ +add_action('add_meta_boxes', function () { + add_meta_box( + 'check_domain_health_box', + __('Check Domain Health', 'rl-mailwarmer'), + 'rl_mailwarmer_check_domain_health_box_callback', + 'domain', + 'side', + 'default' + ); + add_meta_box( + 'create_dns_backup_box', + __('Create DNS Backup', 'rl-mailwarmer'), + 'rl_mailwarmer_render_create_dns_backup_box', + 'domain', + 'side', + 'default' + ); + add_meta_box( + 'fix_deliverability_dns_issues_box', // Meta box ID + __('Fix Deliverability DNS Issues', 'rl-mailwarmer'), // Title + 'rl_mailwarmer_render_fix_deliverability_dns_issues_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 + 'rl_mailwarmer_render_update_spf_record_box', // Callback function + 'domain', // Post type + 'side', // Context + 'default' // Priority + ); + add_meta_box( + 'update_dmarc_record_box', // Meta box ID + __('Update DMARC Record', 'rl-mailwarmer'), // Title + 'rl_mailwarmer_render_update_dmarc_record_box', // Callback function + 'domain', // Post type + 'side', // Context + 'default' // Priority + ); + add_meta_box( + 'update_dkim_record_box', // Meta box ID + __('Update DKIM Record', 'rl-mailwarmer'), // Title + 'rl_mailwarmer_render_update_dkim_record_box', // Callback function + 'domain', // Post type + 'side', // Context + 'default' // Priority + ); +}); + +/** + * Callback for the "Check Domain Health" meta box. + */ +function rl_mailwarmer_check_domain_health_box_callback($post) +{ + // Add nonce for security + wp_nonce_field('check_domain_health_nonce', 'check_domain_health_nonce_field'); + + // Render the button + echo '

'; + echo ''; + echo '

'; + + // Output a placeholder for results + echo '
'; +} + +/** + * Render the meta box for DNS backup. + * + * @param WP_Post $post The current post object. + */ +function rl_mailwarmer_render_create_dns_backup_box($post) +{ + // Add a nonce field for security + wp_nonce_field('create_dns_backup_nonce', 'create_dns_backup_nonce_field'); + + // Render the button + ?> +

+ +

+
+ +

+ +

+
+ +

+
+ +

+

+
+ +

+

+
+ +

+

+
+ +

+

+ +

+
+ +

+
+ +

+

+
+ +

+

+
+ +

+

+
+ +

+

+
+ +

+

+
+ +

+

+
+ +

+

+
+ +

+

+
+ +

+

+
+ +

+

+ +

+
+ +

+
+ +

+

+
+ +

+

+
+ +

+

+
+ +

+

+ +

+
+ __('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'), + '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'), + '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'), + 'dkim_records' => __('DKIM Records', 'rl-mailwarmer'), + ]; + + return array_merge($columns, $custom_columns); +}); + +/** + * Populate custom column data for the "All Domain Health Reports" admin page. + * + * @param string $column The column name. + * @param int $post_id The post ID. + */ +add_action('manage_domain-health-report_posts_custom_column', function ($column, $post_id) { + $meta = get_post_meta($post_id); + + switch ($column) { + case 'domain_name': + echo esc_html($meta['domain_name'][0] ?? ''); + break; + + case 'domain_valid': + echo !empty($meta['domain_valid'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); + break; + + case 'domain_age': + echo esc_html($meta['domain_age'][0] ?? ''); + break; + + case 'domain_days_to_expiration': + echo esc_html($meta['domain_days_to_expiration'][0] ?? ''); + break; + + case 'a_record_valid': + echo !empty($meta['a_record_valid'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); + break; + + case 'a_record_resolves': + echo esc_html($meta['a_record_resolves'][0] ?? ''); + break; + + case 'http_status': + echo esc_html($meta['http_status'][0] ?? ''); + break; + + case 'https_enabled': + echo !empty($meta['https_enabled'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); + break; + + case 'mx_record_valid': + echo !empty($meta['mx_record_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 'spf_record_exists': + echo !empty($meta['spf_record_exists'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); + break; + + case 'spf_record_content': + echo !empty($meta['spf_record_content'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); + 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 '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_sp_policy': + echo esc_html($meta['dmarc_sp_policy'][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_adkim': + echo esc_html($meta['dmarc_adkim'][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_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 'dkim_records': + echo esc_html($meta['dkim_records'][0] ?? ''); + break; + + default: + echo ''; + } +}, 10, 2); + + +// Make Columns Sortable +add_filter('manage_edit-domain-health-report_sortable_columns', function ($columns) { + $columns['domain_name'] = 'domain_name'; + $columns['domain_valid'] = 'domain_valid'; + $columns['domain_age'] = 'domain_age'; + // Add more sortable columns as needed + return $columns; +}); + + +/** + * Add custom columns to the "All Domains" admin page. + * + * @param array $columns Default columns. + * @return array Modified columns. + */ +add_filter('manage_domain_posts_columns', function ($columns) { + // Remove default columns if necessary + // unset($columns['date']); + + // Add custom columns + $custom_columns = [ + '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'), + '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'), + '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'), + 'dkim_records' => __('DKIM Records', 'rl-mailwarmer'), + ]; + + return array_merge($columns, $custom_columns); +}); + +/** + * Populate custom column data for the "All Domains" admin page. + * + * @param string $column The column name. + * @param int $post_id The post ID. + */ +add_action('manage_domain_posts_custom_column', function ($column, $post_id) { + $meta = get_post_meta($post_id); + + switch ($column) { + + case 'domain_valid': + echo !empty($meta['domain_valid'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); + break; + + case 'domain_age': + echo esc_html($meta['domain_age'][0] ?? ''); + break; + + case 'domain_days_to_expiration': + echo esc_html($meta['domain_days_to_expiration'][0] ?? ''); + break; + + case 'a_record_valid': + echo !empty($meta['a_record_valid'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); + break; + + case 'a_record_resolves': + echo esc_html($meta['a_record_resolves'][0] ?? ''); + break; + + case 'http_status': + echo esc_html($meta['http_status'][0] ?? ''); + break; + + case 'https_enabled': + echo !empty($meta['https_enabled'][0]) ? __('Yes', 'rl-mailwarmer') : __('No', 'rl-mailwarmer'); + break; + + case 'mx_record_valid': + echo !empty($meta['mx_record_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 '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_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 '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_sp_policy': + echo esc_html($meta['dmarc_sp_policy'][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_adkim': + echo esc_html($meta['dmarc_adkim'][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_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 'dkim_records': + echo esc_html($meta['dkim_records'][0] ?? ''); + break; + + default: + echo ''; + } +}, 10, 2); + +// Make Columns Sortable +add_filter('manage_edit-domain_sortable_columns', function ($columns) { + // $columns['domain_name'] = 'domain_name'; + $columns['domain_valid'] = 'domain_valid'; + $columns['domain_age'] = 'domain_age'; + // Add more sortable columns as needed + return $columns; +}); + +/** + * Add a custom meta box to display domain metadata. + */ +add_action('add_meta_boxes', function () { + add_meta_box( + 'domain_metadata_table', // Meta box ID + __('Domain Health', 'rl-mailwarmer'), // Title + 'rl_mailwarmer_render_domain_metadata_table', // Callback function + 'domain', // Post type + 'normal', // Context + 'low' // Priority + ); +}); + +/** + * Render the metadata table for the "Domain Metadata" meta box. + * + * @param WP_Post $post The current post object. + */ +function rl_mailwarmer_render_domain_metadata_table($post) +{ + // Fetch all metadata for the current post + $post_meta = get_post_meta($post->ID); + + // 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] ?? '', + ]; + + // Render the table + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + foreach ($metadata as $key => $value) { + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo ''; + echo '
' . __('Title', 'rl-mailwarmer') . '' . __('Value', 'rl-mailwarmer') . '
' . esc_html(ucwords(str_replace('_', ' ', $key))) . '' . esc_html(is_array($value) ? json_encode($value) : $value) . '
'; +} + diff --git a/includes/rl-mailwarmer-functions.php b/includes/rl-mailwarmer-functions.php new file mode 100644 index 0000000..f5b3f69 --- /dev/null +++ b/includes/rl-mailwarmer-functions.php @@ -0,0 +1,76 @@ +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; + } else if ($data) { + $message = $message . " " . $data; + } + + error_log("[$date] " . $message ."\r\n",3,$log_File); + } +} + + +/** + * Save a domain health report when a domain is first published. + * + * @param string $new_status The new post status. + * @param string $old_status The old post status. + * @param WP_Post $post The post object. + */ +// add_action('transition_post_status', function ($new_status, $old_status, $post) { +// // Ensure we're working with the 'domain' post type +// if ($post->post_type !== 'domain') { +// return; +// } + +// // Only run when the status changes to 'publish' from a non-published status +// if ($new_status === 'publish' && $old_status !== 'publish') { +// try { +// RL_MailWarmer_Domain_Helper::saveDomainHealthReport($post->ID); +// } catch (Exception $e) { +// error_log('Failed to save domain health report: ' . $e->getMessage()); +// } +// } +// }, 10, 3); + + + +/** + * Save a domain health report when a new domain is created. + * + * @param int $post_id The ID of the post being saved. + * @param WP_Post $post The post object. + * @param bool $update Whether this is an update. + */ +// add_action('save_post_domain', function ($post_id, $post, $update) { +// // Exclude autosaves, revisions, drafts, and updates +// if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id) || $post->post_status === 'draft' || $update) { +// return; +// } + +// // Call saveDomainHealthReport +// try { +// log_to_file("save_post_domain - Running health report for newly added domain: " . $post->post_title); +// RL_MailWarmer_Domain_Helper::saveDomainHealthReport($post_id); +// } catch (Exception $e) { +// error_log('Failed to save domain health report: ' . $e->getMessage()); +// } + +// log_to_file("save_post_domain - Finished! " . $post->post_title); +// }, 10, 3); diff --git a/includes/rl-mailwarmer-rest.php b/includes/rl-mailwarmer-rest.php new file mode 100644 index 0000000..bc85062 --- /dev/null +++ b/includes/rl-mailwarmer-rest.php @@ -0,0 +1,17 @@ +.+)', [ + 'methods' => 'GET', + 'callback' => function ($data) { + return RL_MailWarmer_Domain_Helper::check_domain_health($data['domain']); + }, + ]); + + register_rest_route('rl-mailwarmer/v1', '/generate-domain-report/(?P.+)', [ + 'methods' => 'GET', + 'callback' => function ($data) { + return RL_MailWarmer_Domain_Helper::generateDomainReport($data['domain']); + }, + ]); +}); diff --git a/js/admin-check-domain-health.js b/js/admin-check-domain-health.js new file mode 100644 index 0000000..da473ce --- /dev/null +++ b/js/admin-check-domain-health.js @@ -0,0 +1,34 @@ +jQuery(document).ready(function ($) { + $('#check-domain-health-button').on('click', function (e) { + e.preventDefault(); + + //var postId = $('#post_ID').val(); // Get the current post ID + var postData = { + action: 'rl_mailwarmer_check_domain_health', + post_id: rlMailWarmer.post_id, + security: rlMailWarmer.nonce, + }; + // console.log("AJAX URL: " + rlMailWarmer.ajax_url); + // console.log("Post Action: " + postData.action); + // console.log("Post postId: " + postData.post_id); + // console.log("Post security: " + postData.security); + + $('#domain-health-result').html('

Checking domain health...

'); + + $.ajax({ + url: rlMailWarmer.ajax_url, + type: 'POST', + data: postData, + success: function (response) { + if (response.success) { + $('#domain-health-result').html('

Report saved successfully. Post ID: ' + response.data + '

'); + } else { + $('#domain-health-result').html('

Error: ' + response.data + '

'); + } + }, + error: function (xhr, status, error) { + $('#domain-health-result').html('

AJAX Error: ' + error + '

'); + }, + }); + }); +}); diff --git a/js/admin-dns-backup.js b/js/admin-dns-backup.js new file mode 100644 index 0000000..207ac19 --- /dev/null +++ b/js/admin-dns-backup.js @@ -0,0 +1,29 @@ +jQuery(document).ready(function ($) { + $('#create-dns-backup-button').on('click', function (e) { + e.preventDefault(); + + const postId = $('#post_ID').val(); + + $('#dns-backup-result').html('

Creating DNS backup...

'); + + $.ajax({ + url: rlMailWarmerDnsBackup.ajax_url, + method: 'POST', + data: { + action: 'rl_mailwarmer_create_dns_backup', + post_id: postId, + security: rlMailWarmerDnsBackup.nonce, + }, + success: function (response) { + if (response.success) { + $('#dns-backup-result').html('

DNS backup created successfully. Backup ID: ' + response.data + '

'); + } else { + $('#dns-backup-result').html('

Error: ' + response.data + '

'); + } + }, + error: function (xhr, status, error) { + $('#dns-backup-result').html('

AJAX Error: ' + error + '

'); + }, + }); + }); +}); diff --git a/js/admin-fix-dns.js b/js/admin-fix-dns.js new file mode 100644 index 0000000..0dd5e15 --- /dev/null +++ b/js/admin-fix-dns.js @@ -0,0 +1,44 @@ +jQuery(document).ready(function ($) { + $('#fix-dns-issues-button').on('click', function (e) { + e.preventDefault(); + + // const postId = $('#post_ID').val(); + const postId = rlMailWarmerDnsFix.post_id; + + $('#dns-issues-fix-result').html('

Fixing DNS issues...

'); + + $.ajax({ + url: rlMailWarmerDnsFix.ajax_url, + method: 'POST', + data: { + action: 'rl_mailwarmer_fix_dns_issues', + post_id: postId, + security: rlMailWarmerDnsFix.nonce, + }, + success: function (response) { + console.log(response); + if (response.success) { + let result = '

DNS Issues Fixed:

'; + $('#dns-issues-fix-result').html(result); + } else { + $('#dns-issues-fix-result').html('

Error: ' + response.data + '

'); + } + }, + error: function (xhr, status, error) { + $('#dns-issues-fix-result').html('

AJAX Error: ' + error + '

'); + }, + }); + }); +}); diff --git a/js/admin-update-dkim.js b/js/admin-update-dkim.js new file mode 100644 index 0000000..746ce8c --- /dev/null +++ b/js/admin-update-dkim.js @@ -0,0 +1,38 @@ +jQuery(document).ready(function ($) { + $('#update-dkim-record-button').on('click', function (e) { + e.preventDefault(); + + // const postId = $('#post_ID').val(); + const postId = rlMailWarmerDkim.post_id; + const selector = $('#dkim_selector').val(); + const action = $('#dkim_action').val(); + const value = $('#dkim_value').val(); + const ttl = $('#dkim_ttl').val(); + + $('#dkim-update-result').html('

Updating DKIM record...

'); + + $.ajax({ + url: rlMailWarmerDkim.ajax_url, + method: 'POST', + data: { + action: 'rl_mailwarmer_update_dkim_record', + post_id: postId, + selector: selector, + action_type: action, + value: value, + ttl: ttl, + security: rlMailWarmerDkim.nonce, + }, + success: function (response) { + if (response.success) { + $('#dkim-update-result').html('

' + response.data + '

'); + } else { + $('#dkim-update-result').html('

Error: ' + response.data + '

'); + } + }, + error: function (xhr, status, error) { + $('#dkim-update-result').html('

AJAX Error: ' + error + '

'); + }, + }); + }); +}); diff --git a/js/admin-update-dmarc.js b/js/admin-update-dmarc.js new file mode 100644 index 0000000..5457b42 --- /dev/null +++ b/js/admin-update-dmarc.js @@ -0,0 +1,40 @@ +jQuery(document).ready(function ($) { + $('#update-dmarc-record-button').on('click', function (e) { + e.preventDefault(); + + const postId = rlMailWarmerDmarc.post_id; + const data = { + action: 'rl_mailwarmer_update_dmarc_record', + post_id: postId, + security: rlMailWarmerDmarc.nonce, + policy: $('#dmarc_policy').val(), + sp: $('#dmarc_sp').val(), + pct: $('#dmarc_pct').val(), + aspf: $('#dmarc_aspf').val(), + adkim: $('#dmarc_adkim').val(), + rua: $('#dmarc_rua').val(), + ruf: $('#dmarc_ruf').val(), + fo: $('#dmarc_fo').val(), + rf: $('#dmarc_rf').val(), + ri: $('#dmarc_ri').val(), + }; + + $('#dmarc-update-result').html('

Updating DMARC record...

'); + + $.ajax({ + url: rlMailWarmerDmarc.ajax_url, + method: 'POST', + data: data, + success: function (response) { + if (response.success) { + $('#dmarc-update-result').html('

' + response.data + '

'); + } else { + $('#dmarc-update-result').html('

Error: ' + response.data + '

'); + } + }, + error: function (xhr, status, error) { + $('#dmarc-update-result').html('

AJAX Error: ' + error + '

'); + }, + }); + }); +}); diff --git a/js/admin-update-spf.js b/js/admin-update-spf.js new file mode 100644 index 0000000..ad43cbe --- /dev/null +++ b/js/admin-update-spf.js @@ -0,0 +1,38 @@ +jQuery(document).ready(function ($) { + $('#update-spf-record-button').on('click', function (e) { + e.preventDefault(); + + // const postId = $('#post_ID').val(); + const postId = rlMailWarmerSpf.post_id; + const host = $('#spf_host').val(); + const action = $('#spf_action').val(); + const allPolicy = $('#spf_all_policy').val(); + const ttl = $('#spf_ttl').val(); + + $('#spf-update-result').html('

Updating SPF record...

'); + + $.ajax({ + url: rlMailWarmerSpf.ajax_url, + method: 'POST', + data: { + action: 'rl_mailwarmer_update_spf_record', + post_id: postId, + host: host, + action_type: action, + all_policy: allPolicy, + ttl: ttl, + security: rlMailWarmerSpf.nonce, + }, + success: function (response) { + if (response.success) { + $('#spf-update-result').html('

' + response.data + '

'); + } else { + $('#spf-update-result').html('

Error: ' + response.data + '

'); + } + }, + error: function (xhr, status, error) { + $('#spf-update-result').html('

AJAX Error: ' + error + '

'); + }, + }); + }); +}); diff --git a/rl-mailwarmer.php b/rl-mailwarmer.php new file mode 100644 index 0000000..56af6a7 --- /dev/null +++ b/rl-mailwarmer.php @@ -0,0 +1,69 @@ +