Skip to content

Instantly share code, notes, and snippets.

@mehargags
Created December 7, 2025 10:56
Show Gist options
  • Select an option

  • Save mehargags/7828502401f79419ab2a1c0a4d2b29bf to your computer and use it in GitHub Desktop.

Select an option

Save mehargags/7828502401f79419ab2a1c0a4d2b29bf to your computer and use it in GitHub Desktop.
Suppressions.php
<?php
// Set exit code to 0 by default
$exitCode = 0;
// Function to load environment variables from .env file
function loadEnv($path) {
if (!file_exists($path)) {
fwrite(STDERR, "Error: Environment file (.env) not found at {$path}\n");
exit(1);
}
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0 || strpos($line, '=') === false) {
continue;
}
list($name, $value) = explode('=', $line, 2);
$name = trim($name);
$value = trim($value, "'\" \t\n\r\0\x0B");
if (function_exists('putenv')) {
putenv(sprintf('%s=%s', $name, $value));
}
$_ENV[$name] = $value;
$_SERVER[$name] = $value;
}
}
// Load environment variables into the global scope
loadEnv(__DIR__ . '/.env');
function run() {
// --- API Key & DB Configuration ---
$apiKey = getenv('ZOHO_SEND_API_IndAstro');
if (empty($apiKey)) {
fwrite(STDERR, "Configuration Error: ZOHO_SEND_API_IndAstro not found in .env file.\n");
return 1;
}
$dbHost = 'localhost';
$dbName = getenv('DBNAME');
$dbUser = getenv('DBUSER');
$dbPass = getenv('DBPASS');
if (empty($dbName) || empty($dbUser) || empty($dbPass)) {
fwrite(STDERR, "Configuration Error: Database credentials are not set.\n");
return 1;
}
$baseApiUrl = 'https://campaigns.zoho.com/emailapi/v2/recipients/suppression';
// --- Database Operations & Main Sync Logic ---
// The script uses a try-catch block to handle potential database or API exceptions gracefully.
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
try {
echo "Connecting to the database...\n";
$conn = new mysqli($dbHost, $dbUser, $dbPass, $dbName);
// A prepared statement is used to check if an email already exists in the local database.
// This is efficient and prevents SQL injection.
$select_stmt = $conn->prepare("SELECT email FROM suppressions WHERE email = ?");
// A prepared statement is used for inserting new records.
// This is much faster than running a full INSERT query for each new record.
$insert_stmt = $conn->prepare("INSERT INTO suppressions (email, zuid, suppression_type, added_time, modified_time, additional_data) VALUES (?, ?, ?, ?, ?, ?)");
echo "Fetching suppressed contacts from API in pages...\n";
// --- Initialization of variables for the sync process ---
$start_index = 1; // The starting index for API pagination.
$page_size = 500; // The number of records to fetch per API call.
$total_records_from_api = -1; // Sentinel value to ensure we only get the total count once.
$total_inserted_count = 0; // Counter for new records added in this run.
$total_checked_count = 0; // Counter for total records processed from the API.
// --- Main Sync Loop ---
// The script loops through pages of API data until all records have been checked.
// This "chunk-based" processing is memory-efficient and scalable, as it never loads the full dataset into memory.
do {
// Construct the API URL with pagination parameters.
$apiUrl = $baseApiUrl . "?start_index={$start_index}&end_index=" . ($start_index + $page_size - 1);
// --- API Call ---
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Zoho-zapikey ' . $apiKey,
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$responseBody = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
// --- Error Handling for API Call ---
if ($curlError || $httpCode !== 200) {
fwrite(STDERR, "API Error: HTTP {$httpCode}. " . $curlError . "\nResponse: {$responseBody}\n");
throw new Exception("API request failed on page starting at index {$start_index}.");
}
$data = json_decode($responseBody, true);
if (json_last_error() !== JSON_ERROR_NONE) {
fwrite(STDERR, "API JSON Decode Error: " . json_last_error_msg() . "\n");
throw new Exception("Failed to decode API response for page at index {$start_index}.");
}
// --- Total Record Count ---
// On the first API call, retrieve the total number of records from the response.
// This is used as the termination condition for the main loop.
if ($total_records_from_api === -1) {
$total_records_from_api = $data['total_count'] ?? 0;
if ($total_records_from_api === 0) {
echo "API reports 0 total suppressed contacts. Nothing to do.\n";
break; // Exit do-while loop
}
echo "API reports a total of {$total_records_from_api} records. Starting sync...\n";
}
// --- Process a Page of Records ---
if (isset($data['suppression']) && !empty($data['suppression'])) {
$suppressions_chunk = $data['suppression'];
$chunk_page_count = count($suppressions_chunk);
$chunk_inserted_count = 0;
// Loop through each record in the fetched chunk.
foreach ($suppressions_chunk as $report) {
$total_checked_count++;
$email = $report['address'] ?? null;
if (!$email) continue; // Skip if address is missing, though unlikely.
// Check if the email already exists in the local database.
$select_stmt->bind_param("s", $email);
$select_stmt->execute();
$result = $select_stmt->get_result();
// If the record does not exist locally, insert it.
if ($result->num_rows === 0) {
$zuid = $report['zuid'] ?? null;
$suppression_type = $report['suppression_type'] ?? null;
$added_time = isset($report['added_time']) ? date('Y-m-d H:i:s', $report['added_time'] / 1000) : null;
$modified_time = isset($report['modified_time']) ? date('Y-m-d H:i:s', $report['modified_time'] / 1000) : null;
$additional_data = isset($report['additional_data']) ? json_encode($report['additional_data']) : null;
// Execute the prepared INSERT statement.
$insert_stmt->bind_param("ssssss", $email, $zuid, $suppression_type, $added_time, $modified_time, $additional_data);
if ($insert_stmt->execute()) {
$chunk_inserted_count++;
}
}
}
$total_inserted_count += $chunk_inserted_count;
echo "Page " . ceil($start_index / $page_size) . ": Checked {$chunk_page_count} records, found and inserted {$chunk_inserted_count} new suppressions.\n";
// Move to the next page for the next iteration.
$start_index += $page_size;
} else {
// If the API returns no more records, we can safely stop.
echo "No more records returned from API. Stopping.\n";
break;
}
// The loop continues until the number of checked records matches the total reported by the API.
} while ($total_checked_count < $total_records_from_api);
// --- Cleanup and Final Report ---
$select_stmt->close();
$insert_stmt->close();
$conn->close();
echo "\nSync finished.\n";
echo "-------------------\n";
echo "Total records reported by API: " . ($total_records_from_api > 0 ? $total_records_from_api : 'N/A') . "\n";
echo "Total records checked: {$total_checked_count}\n";
echo "Total new records inserted: {$total_inserted_count}\n";
echo "-------------------\n";
return 0;
} catch (Exception $e) {
fwrite(STDERR, "An error occurred: " . $e->getMessage() . "\n");
// Ensure database connection is closed even if an error occurs.
if (isset($conn) && $conn instanceof mysqli) {
$conn->close();
}
return 1;
}
}
// --- Script Execution ---
$exitCode = run();
exit($exitCode);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment