rl-warmup-plugin/includes/class-rl-mailwarmer-domain-helper.php

1122 lines
43 KiB
PHP

<?php
/**
* Helper functions for the Domain post type.
*/
if (!defined('ABSPATH')) {
exit;
}
use GuzzleHttp\Client;
require 'vendor/autoload.php'; // Ensure you install Guzzle via Composer
class RL_MailWarmer_Domain_Helper
{
/**
* Normalize the domain input (post object, array, or ID).
*
* @param mixed $domain The domain input.
* @return WP_Post|null The domain post object or null if invalid.
*/
private static function get_domain_post($domain)
{
if (is_numeric($domain)) {
return get_post($domain);
} elseif (is_array($domain) && isset($domain['ID'])) {
return get_post($domain['ID']);
} elseif ($domain instanceof WP_Post) {
return $domain;
}
return null;
}
/**
* Get Cloudflare API credentials from the domain post.
*
* @param WP_Post $domain_post The domain post object.
* @return array|null Array with email and key or null if missing.
*/
private static function get_cloudflare_credentials(WP_Post $domain_post)
{
$email = get_post_meta($domain_post->ID, 'cloudflare_api_email', true);
$key = get_post_meta($domain_post->ID, 'cloudflare_api_key', true);
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;
}
}