Skip to content

Instantly share code, notes, and snippets.

@creativebastard
Created February 19, 2025 13:45
Show Gist options
  • Select an option

  • Save creativebastard/942ee59af0eff596489be51236fab146 to your computer and use it in GitHub Desktop.

Select an option

Save creativebastard/942ee59af0eff596489be51236fab146 to your computer and use it in GitHub Desktop.
Script to synchronize TLSA records between Stalwart mail server and CloudFlare. Might be buggy.
<?php
// Config.json should look like this:
// {
// "domain_name": "DOMAIN",
// "cf_api_key": "CFKEY",
// "stalwart_api_key": "STALWARTKEY",
// "stalwart_endpoint_url": "https://YOURSTALWARTURL"
//}
// Load JSON configuration with validation
function loadConfig() {
$configPath = 'config.json';
if (!file_exists($configPath) || !is_readable($configPath)) {
echo "Error: $configPath not found or not readable.\n";
exit(1);
}
$config = json_decode(file_get_contents($configPath), true);
if (json_last_error() !== JSON_ERROR_NONE) {
echo "Error parsing $configPath: " . json_last_error_msg() . "\n";
exit(1);
}
$requiredKeys = ['domain_name', 'cf_api_key', 'stalwart_api_key', 'stalwart_endpoint_url'];
foreach ($requiredKeys as $key) {
if (!isset($config[$key]) || trim($config[$key]) === '') {
echo "Error: Missing or empty value for '$key' in $configPath.\n";
exit(1);
}
}
return array_map('trim', $config);
}
$config = loadConfig();
$domain = $config['domain_name'];
$stalwartKey = $config['stalwart_api_key'];
$stalwartUrl = rtrim($config['stalwart_endpoint_url'], '/') . "/api/dns/records/$domain";
$cfApiKey = $config['cf_api_key'];
echo "Starting TLSA record synchronization for $domain...\n";
// Fetch TLSA records from Stalwart
echo "Fetching TLSA records from Stalwart...\n";
$stalwartResponse = executeCurlRequest($stalwartUrl, 'GET', ['Authorization: Bearer ' . $stalwartKey]);
if (!isset($stalwartResponse['data']) || empty($stalwartResponse['data'])) {
echo "No TLSA records found in Stalwart. Exiting.\n";
exit(0);
}
$stalwartTlsa = array_filter($stalwartResponse['data'], fn($r) => $r['type'] === 'TLSA');
echo "Found " . count($stalwartTlsa) . " TLSA records from Stalwart.\n";
// Fetch Cloudflare Zone ID
echo "Fetching Cloudflare zone ID for $domain...\n";
$cfZoneUrl = "https://api.cloudflare.com/client/v4/zones?name=$domain&status=active";
$cfZoneResponse = executeCurlRequest($cfZoneUrl, 'GET', ['Authorization: Bearer ' . $cfApiKey]);
if (!$cfZoneResponse['success'] || empty($cfZoneResponse['result'])) {
echo "Error: Zone not found for domain $domain.\n";
exit(1);
}
$zoneId = $cfZoneResponse['result'][0]['id'];
echo "Zone ID: $zoneId.\n";
// Fetch existing DNS records from Cloudflare
echo "Fetching existing DNS records from Cloudflare...\n";
$cfRecordsUrl = "https://api.cloudflare.com/client/v4/zones/$zoneId/dns_records";
$cfRecordsResponse = executeCurlRequest($cfRecordsUrl, 'GET', ['Authorization: Bearer ' . $cfApiKey]);
if (!$cfRecordsResponse['success']) {
echo "Error fetching Cloudflare DNS records: " . $cfRecordsResponse['errors'][0]['message'] . "\n";
exit(1);
}
// Rebuild existing TLSA records with normalized content
$existingTlsa = [];
foreach ($cfRecordsResponse['result'] as $record) {
if ($record['type'] === 'TLSA') {
$content = implode(' ', [
$record['data']['usage'],
$record['data']['selector'],
$record['data']['matching_type'],
$record['data']['certificate']
]);
$existingTlsa[] = [
'name' => $record['name'],
'content' => $content,
'id' => $record['id']
];
}
}
echo "Found " . count($existingTlsa) . " TLSA records in Cloudflare.\n";
// Normalize records for comparison
$stalwartNormalized = normalizeRecords($stalwartTlsa);
$cfNormalized = normalizeRecords($existingTlsa);
// Output normalized records for debugging
echo "Comparing Stalwart and Cloudflare TLSA records...\n";
echo "Normalized Stalwart records:\n";
foreach ($stalwartNormalized as $record) {
echo "- {$record['name']} ({$record['content']})\n";
}
echo "\nNormalized Cloudflare records:\n";
foreach ($cfNormalized as $record) {
echo "- {$record['name']} ({$record['content']})\n";
}
// Check if records match
if (compareRecords($stalwartNormalized, $cfNormalized)) {
echo "\nTLSA records are already up to date.\n";
exit(0);
}
// Update records if mismatched
echo "\nUpdating TLSA records...\n";
// Delete existing Cloudflare TLSA records
if (!empty($existingTlsa)) {
echo "Deleting existing Cloudflare TLSA records:\n";
foreach ($existingTlsa as $record) {
$deleteUrl = "https://api.cloudflare.com/client/v4/zones/$zoneId/dns_records/{$record['id']}";
$result = executeCurlRequest($deleteUrl, 'DELETE', ['Authorization: Bearer ' . $cfApiKey, 'Content-Type: application/json']);
if ($result['success']) {
echo "Deleted: {$record['name']} ({$record['content']})\n";
} else {
echo "Delete failed: {$record['name']} - " . $result['errors'][0]['message'] . "\n";
}
}
}
// Add new Stalwart TLSA records to Cloudflare
echo "Adding updated TLSA records to Cloudflare:\n";
foreach ($stalwartNormalized as $stalwartRecord) {
// Split content into components
$contentParts = explode(' ', trim($stalwartRecord['content']), 4);
if (count($contentParts) !== 4) {
echo "Invalid TLSA format for {$stalwartRecord['name']}: " . $stalwartRecord['content'] . "\n";
continue;
}
list($usage, $selector, $matching_type, $certificate) = $contentParts;
// Prepare data payload
$dnsData = [
'type' => 'TLSA',
'name' => $stalwartRecord['name'],
'data' => [
'usage' => $usage,
'selector' => $selector,
'matching_type' => $matching_type,
'certificate' => $certificate
],
'proxied' => false,
'ttl' => 120
];
// Create record
$addUrl = "https://api.cloudflare.com/client/v4/zones/$zoneId/dns_records";
$result = executeCurlRequest($addUrl, 'POST', ['Authorization: Bearer ' . $cfApiKey, 'Content-Type: application/json'], json_encode($dnsData));
if ($result['success']) {
echo "Added: {$dnsData['name']} ({$dnsData['data']['certificate']})\n";
} else {
echo "Add failed: {$dnsData['name']} - " . $result['errors'][0]['message'] . "\n";
}
}
echo "\nTLSA record synchronization completed.\n";
// Helper function to execute cURL requests
function executeCurlRequest($url, $method = 'GET', $headers = [], $postFields = null) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if (in_array($method, ['POST', 'PUT', 'PATCH']) && $postFields !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
}
$response = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
throw new Exception("cURL Error: $error");
}
return json_decode($response, true);
}
// Helper function to normalize records
function normalizeRecords($records) {
$normalized = [];
foreach ($records as $record) {
$cleanName = rtrim(strtolower(trim($record['name'])), '.');
$contentParts = explode(' ', trim($record['content']));
$contentParts = array_map('trim', $contentParts);
$contentParts = array_slice($contentParts, 0, 4);
$normalizedContent = implode(' ', $contentParts);
$normalized[] = [
'name' => $cleanName,
'content' => $normalizedContent
];
}
return $normalized;
}
// Helper function to compare normalized records
function compareRecords($stalwart, $cloudflare) {
// Build arrays of keys for comparison
$stalwartKeys = [];
foreach ($stalwart as $record) {
$stalwartKeys[$record['name']][$record['content']] = true;
}
$cfKeys = [];
foreach ($cloudflare as $record) {
$cfKeys[$record['name']][$record['content']] = true;
}
// Check if both arrays have the same keys and values
return $stalwartKeys == $cfKeys;
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment