rl-warmup-plugin/includes/class-rl-mailwarmer-domain-helper.php
ruben de40085318 Implement AI-powered conversation generation and improved campaign timeline management
- Add OpenAI integration for realistic email conversation generation
- Enhance campaign timeline algorithm with natural distribution patterns
- Improve message handling with Symfony Mailer components
- Add conversation blueprint system for structured email threads
- Implement visual timeline with heatmap for campaign tracking

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-07 01:49:49 -06:00

1404 lines
56 KiB
PHP

<?php
/**
* Helper functions for the Domain post type.
*/
if (!defined('ABSPATH')) {
exit;
}
use GuzzleHttp\Client;
class RL_MailWarmer_Domain_Helper {
/**
* Get the domain post object by ID or name.
*
* @param mixed $domain The domain ID or name.
* @return WP_Post|null The domain post object or null if not found.
*/
public static function get_domain_post($domain) {
if ( is_a( $domain, 'WP_Post' ) ) {
// log_to_file("get_domain_post - Already a WP Post");
return $domain;
} else if (is_numeric($domain)) {
// log_to_file("get_domain_post - Fetching WP Post by ID");
return get_post($domain);
}
// log_to_file("get_domain_post - Fetching WP Post by Title");
return get_page_by_title($domain, OBJECT, 'domain');
}
/**
* Get CloudFlare credentials from the domain post.
*
* @param WP_Post $domain_post The domain post object.
* @return array Associative array containing `api_email`, `api_key`, and `zone_id`.
* @throws Exception If credentials are missing.
*/
public static function get_cloudflare_credentials($domain) {
$domain_post = self::get_domain_post($domain);
$domain_name = $domain_post->post_title;
$api_email = get_post_meta($domain_post->ID, 'cloudflare_api_email', true);
$api_key = get_post_meta($domain_post->ID, 'cloudflare_api_key', true);
$zone_id = get_post_meta($domain_post->ID, 'cloudflare_zone_id') ? get_post_meta($domain_post->ID, 'cloudflare_zone_id', true) : false;
// If either credential is missing from domain, check user meta
if (!$api_email || !$api_key) {
$owner_id = get_post_meta($domain_post->ID, 'owner_id', true);
// log_to_file("get_cloudflare_credentials - API credentials not saved to domain. Trying to fetch them from the owner: $owner_id");
if ($owner_id) {
if (!$api_email) {
$api_email = get_user_meta($owner_id, 'cloudflare_api_email', true);
}
if (!$api_key) {
$api_key = get_user_meta($owner_id, 'cloudflare_api_key', true);
}
} else {
throw new Exception(__("get_cloudflare_credentials - CloudFlare credentials are incomplete for domain: {$domain_post->ID}.", "rl-mailwarmer"));
}
}
// Fetch & save the Zone ID if we don't already have it
if (!$zone_id) {
$client = new \GuzzleHttp\Client([
'base_uri' => 'https://api.cloudflare.com/client/v4/',
'headers' => [
'Content-Type' => 'application/json',
],
]);
// log_to_file(" - Client API Key: ", $client->getConfig('api_key'));
try {
$response = $client->get('zones', [
'headers' => [
'Authorization' => "Bearer {$api_key}",
'Content-Type' => 'application/json',
],
'query' => ['name' => $domain_name],
]);
$data = json_decode($response->getBody()->getContents(), true);
if (isset($data['result'][0]['id'])) {
$zone_id = $data['result'][0]['id'];
update_post_meta($domain_post->ID,'cloudflare_zone_id',$zone_id);
log_to_file("get_cloudflare_credentials - Saved Zone ID to $domain_name: $zone_id");
}
// throw new Exception(__('get_cloudflare_credentials - Zone ID not found for the domain.', 'rl-mailwarmer'));
} catch (Exception $e) {
throw new Exception(__('get_cloudflare_credentials - Failed to fetch CloudFlare zone ID: ', 'rl-mailwarmer') . $e->getMessage());
}
}
if (!$api_email || !$api_key || !$zone_id) {
return false;
} else {
return compact('api_email', 'api_key', 'zone_id');
}
}
/**
* Establish a connection to the CloudFlare API.
*
* @param array $credentials The CloudFlare credentials.
* @return GuzzleHttp\Client The Guzzle client for API requests.
*/
private static function get_cloudflare_client($domain) {
$domain_post = self::get_domain_post($domain);
if (!$domain_post) {
throw new Exception(__('get_cloudflare_client - Invalid domain specified.', 'rl-mailwarmer'));
}
$domain_name = $domain_post->post_title;
try {
$credentials = self::get_cloudflare_credentials($domain_post);
} catch (Exception $e) {
throw new Exception(__('get_cloudflare_client - Failed to find CloudFlare zone: ', 'rl-mailwarmer') . $e->getMessage());
return false;
}
if ($credentials) {
$client = new \GuzzleHttp\Client([
'base_uri' => 'https://api.cloudflare.com/client/v4/',
'domain' => $domain_name,
'api_email' => $credentials['api_email'],
'api_key' => $credentials['api_key'],
'zone_id' => $credentials['zone_id'],
'headers' => [
'Content-Type' => 'application/json',
],
]);
return $client;
} else {
log_to_file("get_cloudflare_client - Unable to get cloudflare client for $domain_post->post_title");
return false;
}
}
/**
* Fetch DNS records using the CloudFlare API.
*
* @param string $domain The domain name.
* @param string $type The record type (e.g., TXT, A).
* @return array The DNS records.
* @throws Exception If fetching records fails.
*/
private static function fetch_dns_records($client, $type = null) {
try {
$response = $client->get("zones/{$client->getConfig('zone_id')}/dns_records", [
'headers' => [
'Authorization' => "Bearer {$client->getConfig('api_key')}",
'Content-Type' => 'application/json',
],
'query' => ['type' => $type],
]);
// log_to_file("fetch_dns_records - response: ", $response);
$data = json_decode($response->getBody()->getContents(), true);
if (isset($data['result']) && is_array($data['result'])) {
return $data['result'];
// log_to_file("fetch_dns_records - Data: ", $data['result']);
}
throw new Exception(__('fetch_dns_records - No DNS records found.', 'rl-mailwarmer'));
} catch (Exception $e) {
throw new Exception(__('fetch_dns_records - Failed to fetch DNS records: ', 'rl-mailwarmer') . $e->getMessage());
}
}
/**
* Update or create a DNS record using the Cloudflare API.
*
* @param int $domainID The ID of the parent domain post.
* @param string $domain The domain name.
* @param string $type The record type (e.g., TXT).
* @param string $name The record name.
* @param string $content The record content.
* @param int $ttl The TTL value.
* @return bool True on success.
*/
private static function update_dns_record($domain, $domain_name, $type, $name, $content, $ttl, $priority = 0)
{
// log_to_file("update_dns_record - Updating $name record on $domain_name with: " . $content);
$domain_post = self::get_domain_post($domain);
if (!$domain_post) {
// log_to_file("Invalid domain input");
throw new InvalidArgumentException('Invalid domain input.');
}
$client = self::get_cloudflare_client($domain);
$dns_records = self::fetch_dns_records($client, $type);
// Search existing records first, update it if found
foreach ($dns_records as $record) {
if (($record['name'] === $name) && ($record['type'] === $type)) {
if ($content === $record['content']) {
log_to_file("update_dns_record - Old and new values are the same. Not updating DNS $name record on $domain with: " . $content);
return true;
}
log_to_file("update_dns_record - Match found! Creating backup before updating");
// Backup the existing record before updating
$backup_response = RL_MailWarmer_DB_Helper::insert_dns_backup($domain, $record);
// return RL_MailWarmer_DB_Helper::insert_dns_backup($domain_post->post_title, $data['result']);
// log_to_file("Backup response: $backup_response");
if ( $backup_response ){
log_to_file("update_dns_record - Backup successful! Updating $name record on $domain_name with: " . $content);
// log_to_file("Current value: " . $record['content']);
// Update existing record
$client->put("zones/{$client->getConfig('zone_id')}/dns_records/{$record['id']}", [
'headers' => [
'Authorization' => "Bearer {$client->getConfig('api_key')}",
'Content-Type' => 'application/json',
],
'json' => [
'type' => $type,
'name' => $name,
'content' => $content,
'ttl' => $ttl,
'priority'=> (int) $priority,
],
]);
return true;
} else {
log_to_file("update_dns_record - Error creating Backup DNS Record $name for $domain_post->title. NOT updating DNS record.");
return false;
}
}
}
// Create new record if not found
log_to_file("update_dns_record - Creating new record");
$response = $client->post("zones/{$client->getConfig('zone_id')}/dns_records", [
'headers' => [
'Authorization' => "Bearer {$client->getConfig('api_key')}",
'Content-Type' => 'application/json',
],
'json' => [
'type' => $type,
'name' => $name,
'content' => $content,
'ttl' => (int) $ttl,
'priority'=> (int) $priority,
],
]);
$result = json_decode($response->getBody(), true);
log_to_file("update_dns_record - Result: ", $result);
return true;
}
/**
* Perform a full BIND export for a domain and save a backup
*
* @param mixed $domain The domain ID or name.
* @param array|null $record The specific DNS record to export, or null for all records.
* @return bool True on success, false on failure.
*/
public static function export_dns_zone($domain) {
$domain_post = self::get_domain_post($domain);
if (!$domain_post) {
throw new Exception(__('export_dns_zone - Invalid domain specified.', 'rl-mailwarmer'));
}
$domain_name = $domain_post->post_title;
// log_to_file("export_dns_zone - Attempting to export records for $domain_name");
// $credentials = self::get_cloudflare_credentials($domain_post);
$client = self::get_cloudflare_client($domain_post);
$endpoint = "zones/{$client->getConfig('zone_id')}/dns_records/export";
// if ($record) {
// $endpoint .= "?name=" . urlencode($record['name']) . "&type=" . urlencode($record['type']);
// } else {
// $endpoint .= "/export";
// }
try {
$response = $client->get($endpoint, [
'headers' => [
'Authorization' => "Bearer {$client->getConfig('api_key')}",
'Content-Type' => 'application/json',
],
]);
if ($response->getStatusCode() !== 200) {
$data = json_decode($response->getBody(), true);
throw new \RuntimeException('Failed to export DNS records: ' . json_encode($data['errors'] ?? 'Unknown error'));
}
$data = (string) $response->getBody();
// log_to_file("export_dns_zone - Exported data: ", $data);
if ($data != '') {
$record['name'] = $domain_name;
$record['type'] = "FULL";
$record['content'] = $data;
return RL_MailWarmer_DB_Helper::insert_dns_backup($domain, $record);
// return true;
}
// return 69;
} catch (Exception $e) {
error_log(__('Failed to export DNS records: ', 'rl-mailwarmer') . $e->getMessage());
}
log_to_file("export_dns_zone - Unable to export DNS record for $domain_name");
return false;
}
/**
* Generate a comprehensive email deliverability report for the domain.
*
* @param mixed $domain The domain input (post object, array, or ID).
* @return array The generated report.
*/
public static function generate_domain_report($domain)
{
$domain_post = self::get_domain_post($domain);
if (!$domain_post) {
throw new InvalidArgumentException('generate_domain_report - Invalid domain input.');
}
$domain_name = $domain_post->post_title;
// log_to_file("generate_domain_report - Running generate_domain_report for " . $domain_name);
$client = self::get_cloudflare_client($domain);
// $zone_id = self::get_cloudflare_zone_id($client, $credentials, $domain_post->post_title);
if ($client) {
$dns_records = self::fetch_dns_records($client);
log_to_file("generate_domain_report - All Records: ", $dns_records);
if (isset($dns_records[0])) {
$report = [
'domain_health' => self::check_domain_registration($domain_name),
'a_record' => self::check_a_record($dns_records),
'mx_record' => self::check_mx_record($dns_records),
'spf_record' => self::check_spf_record($dns_records),
'dkim_records' => self::check_dkim_record($dns_records),
'dmarc_record' => self::check_dmarc_record($dns_records),
'blacklists' => self::check_blacklists($domain_name),
];
} else {
$report = [
'domain_health' => self::check_domain_registration($domain_name),
'blacklists' => self::check_blacklists($domain_name),
];
}
} else {
log_to_file("generate_domain_report - Unable to connect to CloudFlare for $domain_name");
$report = [
'domain_health' => self::check_domain_registration($domain_name),
'blacklists' => self::check_blacklists($domain_name),
];
}
// log_to_file("generate_domain_report - Health Report for $domain_name: ", $report);
// return false;
return $report;
}
/**
* Save a domain health report as a custom post.
*
* @param mixed $domain The domain input (post object, array, or ID).
* @return int|WP_Error The post ID of the saved report or WP_Error on failure.
*/
public static function save_domain_health_report($domain)
{
$domain_post = self::get_domain_post($domain);
if (!$domain_post) {
throw new InvalidArgumentException('Invalid domain input.');
}
// log_to_file("save_domain_health_report - Trying to generate report for {$domain_post->post_title}");
// log_to_file("save_domain_health_report - Before Report Generation");
$report = self::generate_domain_report($domain);
// log_to_file("save_domain_health_report - After Report Generation");
// Prepare the post title and content
// $title = "{$domain_post->post_title}-" . current_time('timestamp');
$content = json_encode($report);
// log_to_file("save_domain_health_report - Report JSON Content: ", $content);
$report_id = RL_MailWarmer_DB_Helper::insert_health_report_backup($domain, $content);
if (is_wp_error($report_id)) {
wp_send_json_error($report_id->get_error_message());
// return false;
} else {
update_post_meta($domain_post->ID,'domain_health_report',$content);
// wp_send_json_success($report_id);
return $report_id;
}
}
/**
* Check domain health by verifying WHOIS information and basic attributes.
*
* @param mixed $domain The domain input (post object, array, or ID).
* @return array Domain health details.
*/
private static function check_domain_registration($domain_name)
{
// log_to_file("check_domain_registration - Running check_domain_registration for $domain_name");
// Fetch WHOIS data
$whois_data = shell_exec("whois {$domain_name}");
if (!$whois_data) {
return [
'registration_valid' => false,
'domain_age' => null,
'days_to_expiration' => null,
];
}
// Parse WHOIS data
$registration_valid = true;
$creation_date = null;
$expiration_date = null;
if (preg_match('/Creation Date:\s*(.+)/i', $whois_data, $matches)) {
$creation_date = new DateTime(trim($matches[1]));
}
if (preg_match('/Expiration Date:\s*(.+)/i', $whois_data, $matches)) {
$expiration_date = new DateTime(trim($matches[1]));
} else if (preg_match('/Expiry Date:\s*(.+)/i', $whois_data, $matches)) {
$expiration_date = new DateTime(trim($matches[1]));
}
if (!$creation_date || !$expiration_date || $expiration_date < new DateTime()) {
$registration_valid = false;
}
$domain_age = $creation_date ? (new DateTime())->diff($creation_date)->format('%y years, %m months') : null;
$days_to_expiration = $expiration_date ? (new DateTime())->diff($expiration_date)->days : null;
return [
'registration_valid' => $registration_valid,
'domain_age' => $domain_age,
'days_to_expiration' => $days_to_expiration,
];
}
/**
* Check A record for the domain.
*
* @param mixed $domain The domain input (post object, array, or ID).
* @return array A record details.
*/
private static function check_a_record($dns_records)
{
$domain_name = $dns_records[0]['name'];
// log_to_file("check_a_record - Running check_mx_record for $domain_name");
foreach ($dns_records as $record) {
// Check if the record matches the criteria
// log_to_file("check_a_record - DNS Record: ", $record);
if ( ($record['name'] === $domain_name) && ($record['type'] === 'A') ) {
$ip = $record['content'];
$http_status = self::get_http_status($domain_name);
return [
'ip' => $ip,
'http_status' => $http_status,
'https_enabled'=> $http_status === 200 && self::is_https_enabled($domain_name),
];
}
}
return [
'ip' => false,
'http_status' => false,
'https_enabled'=> false,
];
}
/**
* Get HTTP status code for the domain.
*/
private static function get_http_status($domain)
{
$client = new Client(['timeout' => 10]);
try {
$response = $client->get("http://{$domain}");
return $response->getStatusCode();
} catch (Exception $e) {
return null;
}
}
/**
* Check if HTTPS is enabled for the domain.
*/
private static function is_https_enabled($domain)
{
$client = new Client(['timeout' => 10]);
try {
$response = $client->get("https://{$domain}");
return $response->getStatusCode() === 200;
} catch (Exception $e) {
return false;
}
}
/**
* Check MX record for the domain.
*
* @param mixed $domain The domain input (post object, array, or ID).
* @return array MX record details.
*/
private static function check_mx_record($dns_records)
{
$domain_name = $dns_records[0]['name'];
// log_to_file("check_mx_record - Running check_mx_record for $domain_name");
foreach ($dns_records as $record) {
// Check if the record matches the criteria
if ( ($record['name'] === $domain_name) && ($record['type'] === 'MX') ) {
$host = $record['content'];
$ptr_record = gethostbyaddr(gethostbyname($host));
return [
'host' => $host,
'ptr_valid' => $ptr_record !== false,
'ptr_matches' => $ptr_record === $host,
];
}
}
return [
'host' => false,
'ptr_valid' => false,
'ptr_matches' => false,
];
}
/**
* Update or create an MX record for the specified domain in CloudFlare.
*
* @param mixed $domain The domain (can be ID, post object, or name).
* @param string $host The mail server host.
* @param int $priority The MX record priority.
* @return array|string Response from CloudFlare or error message.
*/
public static function update_mx_record($domain, $content, $priority, $ttl = 3600, $action = NULL) {
try {
// Normalize the domain and get CloudFlare credentials
$domain_post = self::get_domain_post($domain);
$client = self::get_cloudflare_client($domain);
$dns_records = self::fetch_dns_records($client);
$existing_record_id = null;
log_to_file("update_mx_record - DNS Records: ", $dns_records);
if ( !empty($dns_records) && ($dns_records != [])) {
log_to_file("update_mx_record - Searching for existing record");
// throw new Exception('Failed to fetch existing DNS records from CloudFlare.');
foreach ($dns_records as $record) {
if ($record['content'] === $content && $record['priority'] === $priority) {
$existing_record_id = $record['id'];
log_to_file("update_mx_record - Matching record found");
break;
}
}
}
// Prepare the payload for the MX record
// $payload = [
// 'type' => 'MX',
// 'name' => $domain_post->post_title,
// 'content' => $content,
// 'priority' => $priority,
// 'ttl' => $ttl, // Default TTL: 1 hour
// ];
// if ($existing_record_id) {
// // Update the existing MX record
// $response = $client->request('PUT', "zones/$zone_id/dns_records/$existing_record_id", [
// 'json' => $payload,
// ]);
// } else {
// // Create a new MX record
// $response = $client->request('POST', "zones/$zone_id/dns_records", [
// 'json' => $payload,
// ]);
// }
log_to_file("update_mx_record - Attempting to update record");
$response = self::update_dns_record($domain_post->ID, $domain_post->post_title, 'MX', $domain_post->post_title, $content, $ttl, $priority);
// $result = json_decode($response->getBody(), true);
log_to_file("update_mx_record - Result: ", $result);
// if (!$result['success']) {
// throw new Exception('Failed to update or create the MX record in CloudFlare.');
// }
return $response; // Return the CloudFlare response
} catch (Exception $e) {
log_to_file('update_mx_record - Error in update_mx_record: ' . $e->getMessage());
return 'Error: ' . $e->getMessage();
}
}
/**
* Check SPF record for the domain.
*
* @param mixed $domain The domain input (post object, array, or ID).
* @return array SPF record details.
*/
private static function check_spf_record($dns_records)
{
$domain_name = $dns_records[0]['name'];
// log_to_file("check_spf_record - Running check_spf_record for $domain_name");
foreach ($dns_records as $record) {
// log_to_file("Found " . $record['name'] . " - ". $record['content']);
if ( ($record['name'] == $domain_name) && ( strpos($record['content'], 'v=spf1') !== false ) ) {
// log_to_file("Match Found: " . $record['name'] . ": " . $record['content']);
return [
'content' => addslashes($record['content']),
'ttl' => $record['ttl'],
'all_mechanism' => strpos($record['content'], '-all') !== false ? '-all' : (strpos($record['content'], '~all') !== false ? '~all' : 'none'),
];
}
}
return [
'content' => false,
'ttl' => false,
'all_mechanism' => false,
];
}
/**
* Update SPF record for the domain.
*
* @param mixed $domain The domain input (post object, array, or ID).
* @param string $host The hostname or IP to add or remove.
* @param string $action The action to perform: 'add' or 'delete'.
* @param string $all_policy The `all` mechanism to set (default: '-all').
* @param int $ttl The TTL value (default: 3600).
* @return bool True on success.
*/
public static function update_spf_record($domain, $host, $action, $all_policy = '-all', $ttl = 3600)
{
// log_to_file("update_spf_record - Post ID: $domain $host $action $all_policy $ttl");
$domain_post = self::get_domain_post($domain);
if (!$domain_post) {
throw new InvalidArgumentException('update_spf_record - Invalid domain input.');
}
$domain_name = $domain_post->post_title;
// log_to_file("update_spf_record - Post ID: $domain_name $host $action $all_policy $ttl");
// log_to_file("update_spf_record - Updating SPF for $domain_name");
$client = self::get_cloudflare_client($domain);
// log_to_file("update_spf_record - Client created");
$dns_records = self::fetch_dns_records($client, "TXT");
$spf_record = null;
foreach ($dns_records as $record) {
// log_to_file("update_spf_record - Found record: ", $record);
if (($record['name'] === $domain_name) && strpos($record['content'], 'v=spf1') !== false) {
$spf_record = $record;
break;
}
}
// Determine the correct format for the $host
$host_format = filter_var($host, FILTER_VALIDATE_IP) ? "ip4:$host" : "include:$host";
if (!$spf_record) {
// Create a new SPF record
$new_content = "v=spf1 +a +mx $host_format $all_policy";
// Ensure TXT content is wrapped in double quotes
$wrapped_content = '"' . trim($new_content, '"') . '"';
// log_to_file("No record found. Creating new record: $domain_name: $wrapped_content");
return self::update_dns_record($domain_post, $domain_name, 'TXT', $domain_name, $wrapped_content, $ttl);
}
// Update the existing SPF record
$content_parts = explode(' ', $spf_record['content']);
if ($action === 'add') {
if (!in_array($host_format, $content_parts, true)) {
array_splice($content_parts, -1, 0, $host_format); // Insert before `all` mechanism
}
} elseif ($action === 'delete') {
$content_parts = array_filter($content_parts, fn($part) => $part !== $host_format);
}
// Ensure the `all` mechanism is set correctly
$content_parts = array_filter($content_parts, fn($part) => !preg_match('/~?all/', $part));
$content_parts[] = $all_policy .'"';
$updated_content = implode(' ', $content_parts);
// log_to_file("Existing record found. Updating");
// log_to_file("Old record: $domain_post->post_title: " .$record['content']);
// log_to_file("New record: $domain_post->post_title: $updated_content");
return self::update_dns_record($domain_post->ID, $domain_post->post_title, 'TXT', $domain_post->post_title, $updated_content, $ttl);
}
/**
* Check DMARC record for the domain.
*
* @param mixed $domain The domain input (post object, array, or ID).
* @return array DMARC record details.
*/
private static function check_dmarc_record($dns_records)
{
$domain_name = $dns_records[0]['name'];
// log_to_file("check_dmarc_record - Running check_dmarc_record for $domain_name");
foreach ($dns_records as $record) {
if ($record['name'] === "_dmarc.{$domain_name}") {
return [
'exists' => true,
'content' => addslashes($record['content']),
'ttl' => $record['ttl'],
'policy' => preg_match('/p=(\w+)/', $record['content'], $matches) ? $matches[1] : null,
'sp_policy' => preg_match('/sp=(\w+)/', $record['content'], $matches) ? $matches[1] : null,
'percentage' => preg_match('/pct=(\d+)/', $record['content'], $matches) ? $matches[1] : null,
'aggregate_rpt' => preg_match('/rua=mailto:([^;]+)/', $record['content'], $matches) ? $matches[1] : null,
'forensic_rpt' => preg_match('/ruf=mailto:([^;]+)/', $record['content'], $matches) ? $matches[1] : null,
'report_interval' => preg_match('/ri=(\d+)/', $record['content'], $matches) ? $matches[1] : null,
'report_format' => preg_match('/rf=(\w+)/', $record['content'], $matches) ? $matches[1] : null,
'aspf' => preg_match('/aspf=(\w+)/', $record['content'], $matches) ? $matches[1] : null,
'adkim' => preg_match('/adkim=(\w+)/', $record['content'], $matches) ? $matches[1] : null,
];
}
}
return [
'exists' => false,
'content' => false,
'ttl' => false,
'policy' => false,
'sp_policy' => false,
'percentage' => false,
'aggregate_rpt' => false,
'forensic_rpt' => false,
'report_interval' => false,
'report_format' => false,
'aspf' => false,
'adkim' => false,
];
}
/**
* Update DMARC record for the domain.
*
* @param mixed $domain The domain input (post object, array, or ID).
* @param array $params Array of DMARC record parameters to update.
* @return bool True on success.
*/
public static function update_dmarc_record($domain, $params = [])
{
$domain_post = self::get_domain_post($domain);
if (!$domain_post) {
throw new InvalidArgumentException('Invalid domain input.');
}
$domain_name = $domain_post->post_title;
$client = self::get_cloudflare_client($domain);
// Fetch existing DMARC record
$dns_records = self::fetch_dns_records($client, "TXT");
$name = "_dmarc.{$domain_name}";
// log_to_file("update_dmarc_record - Updating DMARC record for $name");
$existing_record = null;
foreach ($dns_records as $record) {
if ($record['name'] === $name) {
log_to_file("update_dmarc_record - Match found: " . $record['name']);
$existing_record = $record;
break;
}
}
// Default DMARC record if $params is empty
if (empty($params)) {
$params = [
'v' => 'DMARC1',
'p' => 'quarantine',
'sp' => 'quarantine',
'pct' => 100,
'aspf' => 'r',
'adkim' => 'r',
];
}
// Parse existing record if available
$content_parts = [];
if ($existing_record) {
parse_str(str_replace('; ', '&', $existing_record['content']), $content_parts);
}
// Update or merge parameters
$updated_params = array_merge($content_parts, $params);
// Build the DMARC record string
$dmarc_content = '';
foreach ($updated_params as $key => $value) {
$dmarc_content .= "{$key}={$value}; ";
}
$dmarc_content = rtrim($dmarc_content, '; '); // Remove trailing semicolon and space
// Ensure TXT content is wrapped in double quotes
$wrapped_content = '"' . trim($dmarc_content, '"') . '"';
// log_to_file("update_dmarc_record - New DMARC value: $wrapped_content");
// $dmarc_content .= '"';
// Update or create the DNS record
if ($existing_record) {
return self::update_dns_record(
$domain_post->ID,
$domain_post->post_title,
'TXT',
$name,
$wrapped_content,
$existing_record['ttl']
);
} else {
return self::update_dns_record(
$domain_post->ID,
$domain_post->post_title,
'TXT',
$name,
$wrapped_content,
3600, // Default TTL
);
}
}
/**
* Check DKIM records for the domain.
*
* @param mixed $domain The domain input (post object, array, or ID).
* @param array $selectors Array of DKIM selectors to check.
* @return array DKIM record details for each selector.
*/
private static function check_dkim_record($dns_records, $selectors = [])
{
$domain_name = $dns_records[0]['name'];
// log_to_file("check_dkim_record - Running check_dkim_record for $domain_name");
$dkim_records = [];
$records = [];
$select_suffix = "._domainkey.{$domain_name}";
$default_selectors = ['google', 'selector1', 'selector2', 'k1', 'k2', 'ctct1', 'ctct2', 'sm', 's1',
's2', 'sig1', 'litesrv', 'zendesk1', 'zendesk2', 'mail', 'email', 'dkim', 'default', '202410', '202406'];
$selectors = array_unique(array_merge($default_selectors, $selectors));
$selectors = array_map(function ($item) use ($select_suffix) {
return $item . $select_suffix;
}, $selectors);
foreach ($dns_records as $record) {
// log_to_file("check_dkim_record - Found DKIM Record: {$record['name']}");
if ( in_array($record['name'], $selectors) ) {
// log_to_file("check_dkim_record - Found {$record['name']}._domainkey");
$records[$record['name']] = [
'ttl' => $record['ttl'],
// 'content'=> $record['content'],
];
// break;
}
}
// $dkim_records = [ "dkim_records" => $records ];
return $records;
}
/**
* Update or create a DKIM record for the domain.
*
* @param mixed $domain The domain input (post object, array, or ID).
* @param string $selector The DKIM selector to update.
* @param string $action The action to perform: 'add' or 'delete'.
* @param string $value The DKIM public key (required for 'add').
* @param int $ttl The TTL value (default: 3600).
* @return bool True on success.
*/
public static function update_dkim_record($domain, $selector, $action, $value = '', $ttl = 3600)
{
$domain_post = self::get_domain_post($domain);
if (!$domain_post) {
throw new InvalidArgumentException('Invalid domain input.');
}
$domain_name = $domain_post->post_title;
$client = self::get_cloudflare_client($domain);
// Fetch existing DMARC record
$dns_records = self::fetch_dns_records($client, "TXT");
$name = "{$selector}._domainkey.{$domain_name}";
// Ensure TXT content is wrapped in double quotes
$wrapped_content = '"' . trim($value, '"') . '"';
foreach ($dns_records as $record) {
if ($record['name'] === $name) {
if ($action === 'delete') {
// Delete existing DKIM record
log_to_file("update_dkim_record - delete_dns_record() is not implemented!");
throw new RuntimeException('delete_dns_record() is not implemented!');
// return self::delete_dns_record($domain_post->post_title, $record['id'], $credentials);
}
if ($action === 'add') {
// Update existing DKIM record
return self::update_dns_record($domain_post->ID, $domain_post->post_title, 'TXT', $name, $wrapped_content, $ttl);
}
}
}
if ($action === 'add') {
// Create new DKIM record if not found
return self::update_dns_record($domain_post->ID, $domain_post->post_title, 'TXT', $name, $wrapped_content, $ttl);
}
return false;
}
/**
* Check if the domain or its associated IP is on any blacklist.
*
* @param mixed $host The domain or IP address to check.
* @return array Blacklist check results.
*/
private static function check_blacklists($host)
{
// log_to_file("check_blacklists - Running check_blacklists for $host");
$blacklist_servers = [
'zen.spamhaus.org',
'bl.spamcop.net',
'b.barracudacentral.org',
];
$results = [];
$ip = filter_var($host, FILTER_VALIDATE_IP) ? $host : gethostbyname($host);
foreach ($blacklist_servers as $server) {
$query = implode('.', array_reverse(explode('.', $ip))) . '.' . $server;
$listed = gethostbyname($query) !== $query;
$results[$server] = $listed ? 'Listed' : 'Not Listed';
}
return [
'host' => $host,
'blacklists' => $results,
];
}
/**
* Fix deliverability-related DNS issues for the given domain.
*
* @param mixed $domain The domain input (post object, array, or ID).
* @return array Results of the actions performed.
*/
public static function fix_deliverability_dns_issues($domain)
{
$domain_post = self::get_domain_post($domain);
if (!$domain_post) {
throw new InvalidArgumentException('Invalid domain input.');
}
$domain_name = $domain_post->post_title;
// Create a full backup of the DNS zone before starting
// log_to_file("fix_deliverability_dns_issues - Exporting DNS zone for $domain_name");
$backup_id = self::export_dns_zone($domain_post);
if (is_wp_error($backup_id)) {
log_to_file("fix_deliverability_dns_issues - Error while exporting DNS zone. Quitting");
wp_send_json_error($backup_id->get_error_message());
}
// log_to_file("fix_deliverability_dns_issues - Done exporting DNS zone for $domain_name");
$results = [];
$client = self::get_cloudflare_client($domain);
// Fetch existing DMARC record
// log_to_file("fix_deliverability_dns_issues - Fetching TXT records for $domain_name");
$dns_records = self::fetch_dns_records($client, "TXT");
// Helper to find specific records
$findRecord = function ($records, $contains, $search_name = false) {
foreach ($records as $record) {
if ($search_name) {
if (strpos($record['name'], $contains) !== false) {
return $record;
}
} else {
if (strpos($record['content'], $contains) !== false) {
return $record;
}
}
}
return null;
};
// SPF
// log_to_file("fix_deliverability_dns_issues - Fixing SPF record for $domain_name");
$default_spf_include = get_field('default_spf_include', 'option');
if ($default_spf_include) {
$spf_record = $findRecord($dns_records, 'v=spf1');
if ($spf_record) {
// log_to_file("fix_deliverability_dns_issues - Current SPF record: ",$spf_record);
if (strpos($spf_record['content'], $default_spf_include) === false) {
// Add default_spf_include to the SPF record
// $updated_spf_content = rtrim(str_replace('-all', '', $spf_record['content'])) . " include:{$default_spf_include} -all";
try {
self::update_spf_record($domain_post, $default_spf_include, 'add');
$results['spf'] = "Updated SPF to include $default_spf_include";
} catch (Exception $e) {
$results['spf'] = 'Error: ' . $e->getMessage();
}
} else {
$results['spf'] = 'SPF record already includes default include.';
}
} else {
try {
$results['spf'] = self::update_spf_record($domain_post, $default_spf_include, 'add');
} catch (Exception $e) {
$results['spf'] = 'Error: ' . $e->getMessage();
}
// $results['spf'] = 'No SPF record found for the domain.';
}
} else {
$results['spf'] = 'No default SPF include found in settings.';
}
// DKIM
// log_to_file("fix_deliverability_dns_issues - Fixing DKIM record for $domain_name");
$servers = get_field('servers', $domain_post->ID);
if ($servers && is_array($servers)) {
$results['dkim'] = [];
foreach ($servers as $server) {
$selector = get_field('dkim_selector', $server);
$value = get_field('dkim_value', $server);
// log_to_file("fix_deliverability_dns_issues - DKIM $selector: $value");
if ($selector && $value) {
$dkim_record = $findRecord($dns_records, "{$selector}._domainkey", true);
if (!$dkim_record) {
try {
$dkim_result = self::update_dkim_record($domain_post, $selector, 'add', $value);
$results['dkim'][$selector] = $dkim_result
? "DKIM record for selector '{$selector}' added successfully."
: "Failed to add DKIM record for selector '{$selector}'.";
} catch (Exception $e) {
$results['dkim'][$selector] = 'Error: ' . $e->getMessage();
}
} else {
$results['dkim'][$selector] = "DKIM record for selector '{$selector}' already exists.";
}
} else {
$results['dkim'][$server] = 'Missing DKIM selector or value for server.';
}
}
} else {
log_to_file("fix_deliverability_dns_issues - No servers selected for the domain. Choosing system default.");
$server = get_field('defaut_mailferno_mx', 'option');
$server_id = $server->ID;
$selector = get_field('dkim_selector', $server_id);
$value = get_field('dkim_value', $server_id);
if ($selector && $value) {
$dkim_record = $findRecord($dns_records, "{$selector}._domainkey", true);
if (!$dkim_record) {
try {
$dkim_result = self::update_dkim_record($domain_post, $selector, 'add', $value);
$results['dkim'][$selector] = $dkim_result
? "DKIM record for selector '{$selector}' added successfully."
: "Failed to add DKIM record for selector '{$selector}'.";
} catch (Exception $e) {
$results['dkim'][$selector] = 'Error: ' . $e->getMessage();
}
} else {
$results['dkim'][$selector] = "DKIM record for selector '{$selector}' already exists.";
}
} else {
$results['dkim'][$server_id] = 'Missing DKIM selector or value for server: ' . $server_id;
}
}
// DMARC
// log_to_file("fix_deliverability_dns_issues - Fixing DMARC record for $domain_name");
$dmarc_record = $findRecord($dns_records, '_dmarc', true);
if (!$dmarc_record) {
// log_to_file("fix_deliverability_dns_issues - No existing DMARC record found. Creating one.");
$dmarc_params = [
'v' => 'DMARC1',
'p' => 'quarantine',
'pct' => 100,
'aspf' => 'relaxed',
'adkim' => 'relaxed',
];
try {
$dmarc_result = self::update_dmarc_record($domain_post, $dmarc_params);
$results['dmarc'] = $dmarc_result
? 'Default DMARC record created successfully.'
: 'Failed to create DMARC record.';
} catch (Exception $e) {
$results['dmarc'] = 'Error: ' . $e->getMessage();
}
} else {
// log_to_file("fix_deliverability_dns_issues - Existing DMARC record found. Doing nothing.");
$results['dmarc'] = "Existing DMARC record found: {$dmarc_record['content']}";
}
// $results['health_report'] = '';
// Save a health report after fixing DNS issues
try {
// log_to_file("fix_deliverability_dns_issues - Running save_domain_health_report() for {$domain_name}");
// $report_id = self::save_domain_health_report($domain_post);
$report = self::generate_domain_report($domain);
$content = json_encode($report);
$report_id = RL_MailWarmer_DB_Helper::insert_health_report_backup($domain, $content);
if (is_wp_error($report_id)) {
wp_send_json_error($report_id->get_error_message());
// return false;
} else {
update_post_meta($domain_post->ID,'domain_health_report',$content);
// wp_send_json_success($report_id);
}
$results['health_report'] = "Health report saved successfully with ID {$report_id}.";
// log_to_file("fix_deliverability_dns_issues - Health report saved successfully with ID {$report_id}");
} catch (Exception $e) {
$results['health_report'] = 'Error saving health report: ' . $e->getMessage();
}
return $results;
}
/**
* Enable Resend for the given domain.
*
* @param mixed $domain The domain input (post object, array, or ID).
* @return array Results of the actions performed.
*/
public static function enabled_resend_for_domain($domain)
{
$domain_post = self::get_domain_post($domain);
if (!$domain_post) {
throw new InvalidArgumentException('Invalid domain input.');
}
}
/*
* TO DO!
*
*/
// modify_domain_on_server - Creates/Updates/Deletes the specified domain on the specified server if it doesn't already exist (VirtualMin). Checks/Updates DNS config to include server IP in SPF, DKIM key, etc.
/**
* Modify a domain account on a VirtualMin server.
*
* Creates, updates, or deletes a virtual server for the specified domain.
*
* @param int $domain_id The domain post ID.
* @param string $action The action to perform: 'create', 'update', or 'delete'.
* @return bool|WP_Error True on success, WP_Error on failure.
*/
private static function modify_domain_account_on_server($domain_id, $action)
{
// Validate domain post
$domain = get_post($domain_id);
if (!$domain || $domain->post_type !== 'domain') {
return new WP_Error('invalid_domain', __('Invalid domain post.', 'rl-mailwarmer'));
}
// Fetch associated server posts
$server_ids = get_post_meta($domain_id, 'associated_servers', true); // Assume this holds server IDs
if (empty($server_ids) || !is_array($server_ids)) {
return new WP_Error('missing_servers', __('No associated servers found for the domain.', 'rl-mailwarmer'));
}
$domain_name = $domain->post_title;
// Iterate over servers and perform the action
foreach ($server_ids as $server_id) {
$server = get_post($server_id);
if (!$server || $server->post_type !== 'server') {
continue; // Skip invalid server posts
}
$server_ip = get_post_meta($server_id, 'ip_address', true);
$server_user = get_post_meta($server_id, 'username', true);
$server_port = get_post_meta($server_id, 'ssh_port', true) ?: 22;
$server_password = get_post_meta($server_id, 'ssh_private_key', true);
if (!$server_ip || !$server_user || !$server_password) {
return new WP_Error('missing_server_credentials', __('Missing server credentials.', 'rl-mailwarmer'));
}
// Build VirtualMin command
$command = "virtualmin";
if ($action === 'create') {
$command .= " create-domain --domain $domain_name --unix --dir --web --email";
} elseif ($action === 'update') {
$command .= " modify-domain --domain $domain_name";
} elseif ($action === 'delete') {
$command .= " delete-domain --domain $domain_name";
} else {
return new WP_Error('invalid_action', __('Invalid action specified.', 'rl-mailwarmer'));
}
// Execute the command via SSH
// $ssh = new phpseclib3\Net\SSH2($server_ip, $ssh_port);
// $key = phpseclib3\Crypt\PublicKeyLoader::loadPrivateKey($server_password); // Adjust for SSH key or plain password
$ssh = new SSH2($server_ip, $server_port);
if (!empty($server_password)) {
// Load the private key from the postmeta field
$key = PublicKeyLoader::loadPrivateKey($server_password);
} else {
// Fallback to password-based authentication
// $key = $server_password;
log_to_file("modify_domain_account_on_server - Server $$server_id ssh_private_key empty");
return new WP_Error('ssh_login_failed', __('No private key found!', 'rl-mailwarmer'));
}
if (!$ssh->login($server_user, $key)) {
return new WP_Error('ssh_login_failed', __('Failed to log into the server.', 'rl-mailwarmer'));
}
$output = $ssh->exec($command);
if (strpos($output, 'failed') !== false) {
return new WP_Error('command_failed', __('Failed to execute VirtualMin command.', 'rl-mailwarmer'));
}
}
return true; // Success
}
}
add_action('restrict_manage_posts', function ($post_type) {
if ($post_type === 'domain') {
?>
<button id="check-domain-health-button" class="button button-primary" style="margin-left: 10px;">
<?php esc_html_e('Check Domain Health', 'rl-mailwarmer'); ?>
</button>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.getElementById('check-domain-health-button').addEventListener('click', function (event) {
event.preventDefault(); // Prevent form submission
const selectedDomains = Array.from(document.querySelectorAll('.check-column input[type="checkbox"]:checked'))
.map(checkbox => checkbox.value)
.filter(value => value !== 'on'); // Exclude the "select all" checkbox
if (selectedDomains.length === 0) {
alert('<?php esc_html_e('Please select at least one domain.', 'rl-mailwarmer'); ?>');
return;
}
if (confirm('<?php esc_html_e('Are you sure you want to check the health of the selected domains?', 'rl-mailwarmer'); ?>')) {
jQuery.post(ajaxurl, {
action: 'rl_check_domain_registration',
domain_ids: selectedDomains
}, function (response) {
alert(response.data.message);
});
}
});
});
</script>
<?php
}
});
add_action('wp_ajax_rl_check_domain_registration', function () {
$domain_ids = isset($_POST['domain_ids']) ? array_map('intval', $_POST['domain_ids']) : [];
if (empty($domain_ids)) {
wp_send_json_error(['message' => __('No domains selected.', 'rl-mailwarmer')]);
}
foreach ($domain_ids as $domain_id) {
// Run the save_domain_health_report function
$result = RL_MailWarmer_Domain_Helper::save_domain_health_report($domain_id);
if (is_wp_error($result)) {
wp_send_json_error(['message' => $result->get_error_message()]);
}
}
wp_send_json_success(['message' => __('Domain health check completed for selected domains.', 'rl-mailwarmer')]);
});
add_action('add_meta_boxes', 'rl_mailwarmer_add_generate_accounts_metabox');
function rl_mailwarmer_add_generate_accounts_metabox() {
add_meta_box(
'rl-generate-accounts',
__('Generate Accounts', 'rl-mailwarmer'),
'rl_mailwarmer_generate_accounts_metabox_callback',
'domain',
'side',
'default'
);
}
function rl_mailwarmer_generate_accounts_metabox_callback($post) {
// Nonce field for security
wp_nonce_field('rl_generate_accounts_nonce', 'rl_generate_accounts_nonce_field');
?>
<p>
<label for="rl_generate_account_qty"><?php _e('Number of Accounts', 'rl-mailwarmer'); ?></label>
<input type="number" id="rl_generate_account_qty" name="rl_generate_account_qty" value="1" min="1" max="50" />
</p>
<button type="button" id="rl_generate_accounts_button" class="button button-primary">
<?php _e('Generate Accounts', 'rl-mailwarmer'); ?>
</button>
<div id="rl_generate_accounts_result"></div>
<?php
}
add_action('admin_enqueue_scripts', 'rl_mailwarmer_enqueue_generate_accounts_script');
function rl_mailwarmer_enqueue_generate_accounts_script($hook) {
if ($hook === 'post.php' || $hook === 'post-new.php') {
wp_enqueue_script(
'rl-generate-accounts',
RL_MAILWARMER_URL . 'js/rl-generate-accounts.js',
['jquery'],
'1.0',
true
);
wp_localize_script('rl-generate-accounts', 'rlGenerateAccounts', [
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('rl_generate_accounts_nonce'),
]);
}
}
add_action('wp_ajax_rl_generate_random_accounts', 'rl_mailwarmer_generate_random_accounts_handler');
function rl_mailwarmer_generate_random_accounts_handler() {
check_ajax_referer('rl_generate_accounts_nonce', 'security');
$post_id = intval($_POST['post_id']);
$qty = intval($_POST['qty']);
if (!$post_id || !$qty || $qty < 1 || $qty > 50) {
wp_send_json_error(__('Invalid input.', 'rl-mailwarmer'));
}
$accounts = RL_MailWarmer_Email_Helper::generate_random_accounts($post_id, $qty);
if (is_array($accounts)) {
wp_send_json_success($accounts);
} else {
wp_send_json_error($accounts);
}
}