| Current Path : /var/www/cesa.co.za/crons/ |
| Current File : /var/www/cesa.co.za/crons/ToSendProcess.php |
<?php
//if ($_GET["override"] != 1) die();
ini_set('max_execution_time', '0'); // 0 = no limit
//ini_set('display_errors', 1);
//error_reporting(E_ALL);
ob_start();
$maxmsgs = 10;
$loopMax = 3;
if (!isset($_SERVER['DOCUMENT_ROOT']) || strlen($_SERVER['DOCUMENT_ROOT']) <= 0) {
$_SERVER['DOCUMENT_ROOT'] = str_replace('/crons', '', getcwd());
// echo getcwd();
}
// Check if process is running using PID-based lock file
$lockFile = $_SERVER['DOCUMENT_ROOT'] . '/crons/ToSendProcess.lock';
if (file_exists($lockFile)) {
$lockContent = file_get_contents($lockFile);
$lockData = explode('|', $lockContent);
$storedPid = isset($lockData[0]) ? intval($lockData[0]) : 0;
$lockTime = isset($lockData[1]) ? intval($lockData[1]) : 0;
// Check if the stored PID is actually running
$processRunning = false;
if ($storedPid > 0) {
// On Linux/Unix, check if process exists
if (function_exists('posix_kill')) {
// Use posix_kill with signal 0 to check if process exists (doesn't actually kill)
$processRunning = @posix_kill($storedPid, 0);
} else {
// Fallback: check using ps command
$psOutput = @shell_exec("ps -p $storedPid -o pid= 2>/dev/null");
$processRunning = !empty(trim($psOutput));
}
}
// Also check if lock file is very old (more than 12 hours) as a safety measure
$currentTime = time();
$lockAge = $currentTime - $lockTime;
$maxLockAge = 60 * 60 * 12; // 12 hours
if ($processRunning) {
// Process is actually running
echo "Process already running (PID: $storedPid). Exiting.\n";
echo "Lock file created at: " . date('Y-m-d H:i:s', $lockTime) . "\n";
exit;
} elseif ($lockAge < $maxLockAge && $storedPid > 0) {
// Lock file is recent but PID doesn't exist - might be a race condition
// Wait a moment and check again
sleep(2);
if (function_exists('posix_kill')) {
$processRunning = @posix_kill($storedPid, 0);
} else {
$psOutput = @shell_exec("ps -p $storedPid -o pid= 2>/dev/null");
$processRunning = !empty(trim($psOutput));
}
if ($processRunning) {
echo "Process already running (PID: $storedPid). Exiting.\n";
exit;
}
}
// Lock file exists but process is not running, or lock is very old - remove it
echo "Removing stale lock file (PID: $storedPid, Age: " . round($lockAge / 3600, 2) . " hours).\n";
unlink($lockFile);
}
// Create the lock file with current PID and timestamp
$currentPid = getmypid();
$lockContent = $currentPid . '|' . time();
file_put_contents($lockFile, $lockContent);
try {
//echo $_SERVER['DOCUMENT_ROOT'];
//exit;
include_once($_SERVER['DOCUMENT_ROOT'] . "/cfg/config.php");
include_once($_SERVER['DOCUMENT_ROOT'] . "/php/includes/ErrorLog.php");
include_once($_SERVER['DOCUMENT_ROOT'] . "/php/includes/maill.php");
include_once($_SERVER['DOCUMENT_ROOT'] . "/php/includes/BaseTableClass.php");
include_once($_SERVER['DOCUMENT_ROOT'] . "/php/includes/mail/MailMessages.php");
//include_once($_SERVER['DOCUMENT_ROOT'] . "/php/test_data/raw_email.php");
echo "ToSendProcess: Script started at " . date('Y-m-d H:i:s') . "\n";
ErrorLog::logInfo('ToSendProcess: Script started at ' . date('Y-m-d H:i:s'));
$db = mysqli_connect(EW_CONN_HOST, MAILDB_USER, MAILDB_PASS, MAILDB_DB);
//$testMailsSQL = "SELECT * FROM Messages ORDER BY RAND() LIMIT 0, 10";
//$testMailsRec = mysqli_query($db, $testMailsSQL);
//while ($testMail = mysqli_fetch_array($testMailsRec)) {
// maill('otto@igotafrica.com', $testMail['Subject'], $testMail['Message'], null, array('headers_raw' => $testMail['Headers']));
//}
$bCanSend = false;
$bSomethingSent = false;
$sql = "SELECT StartSend, EndSend FROM LastSend";
$rsRec = mysqli_query($db, $sql);
if ($rsR = mysqli_fetch_assoc($rsRec)) {
// Has 15 seconds passed since the last send?
if ((time() - strtotime($rsR["StartSend"])) >= 15) {
$bCanSend = true;
// Or, was the last start a loooong time ago?
} elseif ((time() - strtotime($rsR["StartSend"])) >= 10) {
$bCanSend = true;
}
}
ErrorLog::logInfo('ToSendProcess: bCanSend check result: ' . ($bCanSend ? 'true' : 'false'));
if ($bCanSend) {
// Get the messages
$sql = "SELECT * FROM Messages WHERE FromSchool=0 AND SendComplete=0 AND EXISTS (SELECT * FROM ToSend WHERE ToSend.MessageID=Messages.MessageID AND ToSend.Sent=0)
ORDER BY Messages.Prioritise DESC, Messages.MessageID";
$rsMsg = mysqli_query($db, $sql);
if ($rsM = mysqli_fetch_assoc($rsMsg)) {
echo "ToSendProcess: Found message ID " . $rsM["MessageID"] . " with Subject: " . $rsM["Subject"] . "\n";
echo "ToSendProcess: MessageFrom: " . $rsM["MessageFrom"] . "\n";
echo "ToSendProcess: Options: " . substr($rsM["Options"], 0, 200) . "\n";
ErrorLog::logInfo('ToSendProcess: Found message ID ' . $rsM["MessageID"] . ' with MessageFrom: ' . $rsM["MessageFrom"]);
} else {
echo "ToSendProcess: No messages found to send.\n";
ErrorLog::logInfo('ToSendProcess: No messages found - SQL: ' . $sql);
$bCanSend = false;
// maill('cesa-bulk-mail@igotafrica.com', 'CESA Mailing List Error', 'Select from messages failed: ' . $sql);
}
} else {
echo "ToSendProcess: Cannot send yet (timing restrictions).\n";
ErrorLog::logInfo('ToSendProcess: Cannot send yet - timing restrictions not met');
unlink($lockFile);
}
if ($bCanSend) {
for ($loopCnt = 1; $loopCnt <= $loopMax; $loopCnt++) {
//Send 20 messages 10 times
// Make sure another instance of this script doesn't start sending!!
$sql = "UPDATE LastSend SET StartSend='" . date("Y-m-d H:i:s") . "', EndSend='2020-01-01'";
mysqli_query($db, $sql);
$iMsgsSent = 0;
$sql = "SELECT ToSend.ToSendID, ToSend.SendTo, ToSend.Message
FROM ToSend
WHERE ToSend.MessageID=" . $rsM["MessageID"] . " and ToSend.Sent=0 and ToSend.SendTo
LIKE '%@cesa.co.za' ORDER BY ToSend.SendTo LIMIT 0," . $maxmsgs;
// echo $sql . "\r\n";
$rsRec = mysqli_query($db, $sql);
if (mysqli_num_rows($rsRec) <= 0) {
$sql = "SELECT ToSend.ToSendID, ToSend.SendTo, ToSend.Message
FROM ToSend
WHERE ToSend.MessageID=" . $rsM["MessageID"] . " and ToSend.Sent=0
and NOT (ToSend.SendTo LIKE '%@cesa.co.za') ORDER BY ToSend.SendTo LIMIT 0," . $maxmsgs;
// echo $sql . "\r\n";
$rsRec = mysqli_query($db, $sql);
}
if (mysqli_num_rows($rsRec) >= 4000) {
maill('cesa-bulk-mail@igotafrica.com', 'CESA Mailing List Backed Up', 'Messages waiting to be sent: ' . mysqli_num_rows($rsRec));
}
while ($rsR = mysqli_fetch_assoc($rsRec)) {
$sMessage = stripslashes($rsM["Message"]);
if (!empty($rsR["Message"]))
$sMessage = stripslashes($rsR["Message"]);
if (strlen((string) $sMessage) <= 0 || strlen((string) $rsM["Subject"]) <= 0) {
$sql = "UPDATE ToSend SET Sent=2 WHERE ToSendID=" . intval($rsR["ToSendID"]);
mysqli_query($db, $sql);
ErrorLog::logInfo('ToSendProcess: Message ID ' . $rsM["MessageID"] . ' has no message or subject. Skipping. ' . print_r($rsR, true));
continue;
}
// Extract attachments and embedded images from raw email if message looks like raw email
// If ToSend.Message is personalized HTML, try Messages.Message for raw email
$extractedAttachments = [];
$embeddedImages = [];
$finalMessage = $sMessage;
$rawEmailToParse = $sMessage;
// If ToSend.Message doesn't look like raw email but Messages.Message might be, use that
$hasHeaders = (strpos($sMessage, "\r\n\r\n") !== false || strpos($sMessage, "\n\n") !== false);
$hasContentType = (strpos($sMessage, 'Content-Type:') !== false || strpos($sMessage, 'boundary=') !== false);
if (!$hasHeaders || !$hasContentType) {
// ToSend.Message is likely personalized HTML, try Messages.Message
$messagesMessage = stripslashes($rsM["Message"]);
if (!empty($messagesMessage) && $messagesMessage !== $sMessage) {
$msgsHasHeaders = (strpos($messagesMessage, "\r\n\r\n") !== false || strpos($messagesMessage, "\n\n") !== false);
$msgsHasContentType = (strpos($messagesMessage, 'Content-Type:') !== false || strpos($messagesMessage, 'boundary=') !== false);
if ($msgsHasHeaders && $msgsHasContentType) {
$rawEmailToParse = $messagesMessage;
ErrorLog::logInfo('ToSendProcess: Using Messages.Message for raw email parsing (ToSend.Message appears to be personalized)');
}
}
}
// Check if message looks like a raw email (contains headers and multipart structure)
$checkHeaders = (strpos($rawEmailToParse, "\r\n\r\n") !== false || strpos($rawEmailToParse, "\n\n") !== false);
$checkContentType = (strpos($rawEmailToParse, 'Content-Type:') !== false || strpos($rawEmailToParse, 'boundary=') !== false);
$mailparseAvailable = extension_loaded('mailparse');
ErrorLog::logInfo('ToSendProcess: Checking raw email format - mailparse available: ' . ($mailparseAvailable ? 'yes' : 'no') . ', hasHeaders: ' . ($checkHeaders ? 'yes' : 'no') . ', hasContentType: ' . ($checkContentType ? 'yes' : 'no') . ', message to parse length: ' . strlen($rawEmailToParse));
if ($mailparseAvailable && $checkHeaders && $checkContentType) {
ErrorLog::logInfo('ToSendProcess: Message looks like raw email, attempting to parse...');
try {
$parsed = MailMessages::parseRawEmail($rawEmailToParse);
ErrorLog::logInfo('ToSendProcess: Raw email parsed successfully - HTML length: ' . (isset($parsed['html']) ? strlen($parsed['html']) : 0) . ', Text length: ' . (isset($parsed['text']) ? strlen($parsed['text']) : 0) . ', Attachments: ' . (isset($parsed['attachments']) ? count($parsed['attachments']) : 0));
// Use HTML content if available, otherwise text
if (!empty($parsed['html'])) {
$finalMessage = $parsed['html'];
} elseif (!empty($parsed['text'])) {
$finalMessage = $parsed['text'];
}
if (!empty($finalMessage)) {
echo "ToSendProcess: Extracted " . (!empty($parsed['html']) ? 'HTML' : 'text') . " content from raw email, length: " . strlen($finalMessage) . "\n";
}
// Extract attachments and inline images
if (!empty($parsed['attachments']) && is_array($parsed['attachments'])) {
$tempDir = sys_get_temp_dir();
foreach ($parsed['attachments'] as $idx => $attachment) {
if (isset($attachment['content']) && !empty($attachment['content'])) {
$filename = isset($attachment['filename']) ? $attachment['filename'] : 'attachment_' . $idx;
// Clean filename but preserve extension
$pathInfo = pathinfo($filename);
$baseName = isset($pathInfo['filename']) ? $pathInfo['filename'] : $filename;
$extension = isset($pathInfo['extension']) ? '.' . $pathInfo['extension'] : '';
// Clean base name (remove invalid characters)
$baseName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $baseName);
if (empty($baseName)) {
$baseName = 'attachment_' . $idx;
}
// Reconstruct filename with extension preserved
$filename = $baseName . $extension;
// Create temp path with timestamp/random but preserve extension
$tempPath = $tempDir . '/' . $baseName . '_' . time() . '_' . mt_rand(1000, 9999) . $extension;
if (file_put_contents($tempPath, $attachment['content'])) {
// Check if this is an embedded image (has Content-ID and is an image type)
$contentType = $attachment['content_type'] ?? '';
$contentId = isset($attachment['content_id']) ? trim($attachment['content_id'], '<>') : '';
// Debug logging
ErrorLog::logInfo('ToSendProcess: Processing attachment - filename: ' . $filename . ', content_type: ' . $contentType . ', content_id: "' . $contentId . '"');
if (!empty($contentId) && strpos($contentType, 'image/') !== false) {
// This is an embedded image, add it to embedded images
$cid = trim($contentId, '<>');
$embeddedImages[] = [
'cid' => $cid,
'path' => $tempPath,
'content_type' => $contentType
];
echo "ToSendProcess: Found embedded image with CID: " . $cid . " (Content-Type: " . $contentType . ")\n";
ErrorLog::logInfo('ToSendProcess: Added embedded image - CID: "' . $cid . '", Path: ' . $tempPath . ', Content-Type: ' . $contentType);
} else {
// This is a regular attachment
// Store both path and original filename to preserve extension
$extractedAttachments[] = [
'path' => $tempPath,
'name' => $filename // Original filename with extension preserved
];
echo "ToSendProcess: Extracted attachment: " . $filename . " (" . strlen($attachment['content']) . " bytes)\n";
if (empty($contentId)) {
ErrorLog::logInfo('ToSendProcess: Attachment has no Content-ID - treating as regular attachment');
} elseif (strpos($contentType, 'image/') === false) {
ErrorLog::logInfo('ToSendProcess: Attachment has Content-ID but is not an image type - treating as regular attachment');
}
}
}
}
}
ErrorLog::logInfo('ToSendProcess: Extracted ' . count($extractedAttachments) . ' attachments and ' . count($embeddedImages) . ' embedded images from raw email');
}
} catch (Exception $parseEx) {
ErrorLog::logInfo('ToSendProcess: Failed to parse raw email: ' . $parseEx->getMessage() . ' - Trace: ' . $parseEx->getTraceAsString());
// Continue with original message if parsing fails
$finalMessage = $sMessage;
}
} else {
ErrorLog::logInfo('ToSendProcess: Message does not look like raw email - skipping attachment/image extraction. Message preview: ' . substr($sMessage, 0, 200));
}
// Update message to use extracted content
$sMessage = $finalMessage;
// echo $sMessage . ' is the message' . "\r\n";
// Decode quoted-printable if message contains encoded sequences
// But be careful not to corrupt HTML content
$isHtmlMessage = (stripos($sMessage, '<html') !== false || stripos($sMessage, '<body') !== false ||
stripos($sMessage, '<div') !== false || stripos($sMessage, '<p') !== false);
// Only decode when there are clear quoted-printable soft-break markers.
// Decoding solely because "=3D" exists can corrupt normal URLs like "?id=511"
// by interpreting "=51" as quoted-printable hex ("Q").
$hasSoftBreaks = (bool) preg_match('/=\s*\r?\n/', $sMessage);
if ($hasSoftBreaks) {
$decoded = quoted_printable_decode($sMessage);
if (!empty($decoded)) {
$sMessage = $decoded;
}
ErrorLog::logInfo('ToSendProcess: Quoted-printable decoded (HTML: ' . ($isHtmlMessage ? 'true' : 'false') . '), message length: ' . strlen($sMessage));
}
// Verify HTML structure after decoding
if ($isHtmlMessage) {
echo "ToSendProcess: Message is HTML (contains HTML tags)\n";
if (stripos($sMessage, '<html') === false && stripos($sMessage, '<body') === false &&
stripos($sMessage, '<div') === false && stripos($sMessage, '<p') === false) {
echo "ToSendProcess: WARNING - HTML tags lost during processing!\n";
ErrorLog::logInfo('ToSendProcess: WARNING - HTML structure may have been corrupted');
}
}
// Extract from-name and from-address from MessageFrom field if available
$fromName = '';
$fromAddress = '';
if (!empty($rsM['MessageFrom'])) {
$messageFrom = trim($rsM['MessageFrom']);
echo "ToSendProcess: Processing MessageFrom: " . $messageFrom . "\n";
if (strpos($messageFrom, '<') !== false && strpos($messageFrom, '>') !== false) {
// Format: "Name <email@domain.com>"
$fromParts = explode('<', $messageFrom);
$fromName = trim($fromParts[0]);
// Strip quotes (single and double) from the name
$fromName = trim($fromName, '\'"');
if (count($fromParts) > 1) {
$fromAddress = trim(str_replace('>', '', $fromParts[1]));
}
echo "ToSendProcess: Extracted from MessageFrom - Name: \"" . $fromName . "\", Address: \"" . $fromAddress . "\"\n";
} else {
// Format: "email@domain.com" (no name)
$fromAddress = trim($messageFrom);
echo "ToSendProcess: MessageFrom has no name, Address: \"" . $fromAddress . "\"\n";
}
// Debug logging
ErrorLog::logInfo('ToSendProcess: Extracted from MessageFrom - Name: "' . $fromName . '", Address: "' . $fromAddress . '"');
} else {
echo "ToSendProcess: WARNING - MessageFrom is empty!\n";
}
// Prioritize Options over Headers - Options contains the correct content-type and isHTML flag
if ($rsM['Options'] != '') {
echo "ToSendProcess: Processing Options field...\n";
// Try to decode JSON - handle both escaped and non-escaped JSON
$optionsJson = $rsM['Options'];
$options = json_decode($optionsJson, true);
// If decoding failed, try with stripslashes
if ($options === null && json_last_error() !== JSON_ERROR_NONE) {
echo "ToSendProcess: Initial JSON decode failed, trying with stripslashes...\n";
$options = json_decode(stripslashes($optionsJson), true);
ErrorLog::logInfo('ToSendProcess: JSON decode required stripslashes, original: ' . substr($optionsJson, 0, 200));
}
if (is_array($options)) {
echo "ToSendProcess: Options JSON decoded successfully\n";
echo "ToSendProcess: Options - from-name: \"" . (isset($options['from-name']) ? $options['from-name'] : 'not set') . "\", isHTML: " . (isset($options['isHTML']) ? ($options['isHTML'] ? 'true' : 'false') : 'not set') . "\n";
// Preserve the 'source' field to identify bulk mails
if (isset($options['source'])) {
echo "ToSendProcess: Options - source: \"" . $options['source'] . "\" (bulk mail detected)\n";
}
} else {
echo "ToSendProcess: ERROR - Options JSON decode FAILED! Error: " . json_last_error_msg() . "\n";
}
// Debug: Log the Options field value and decoded result
ErrorLog::logInfo('ToSendProcess: Options JSON: ' . substr($optionsJson, 0, 200));
ErrorLog::logInfo('ToSendProcess: Options decoded: ' . print_r($options, true));
// Clean the options to fix escaped slashes and other issues
if (is_array($options)) {
// Fix escaped content-type
if (isset($options['content-type'])) {
$options['content-type'] = str_replace('\\/', '/', $options['content-type']);
}
ErrorLog::logInfo('ToSendProcess: Content type is ' . (isset($options['content-type']) ? $options['content-type'] : 'not set'));
// Ensure isHTML is properly set - prioritize explicit value from Options
if (isset($options['isHTML'])) {
// Use the explicit isHTML value from Options
$options['isHTML'] = (bool)$options['isHTML'];
echo "ToSendProcess: Set isHTML from Options: " . ($options['isHTML'] ? 'true' : 'false') . "\n";
} elseif (isset($options['content-type']) && strpos($options['content-type'], 'text/html') !== false) {
$options['isHTML'] = true;
echo "ToSendProcess: Set isHTML=true from content-type: " . $options['content-type'] . "\n";
} elseif (isset($options['content-type']) && strpos($options['content-type'], 'text/plain') !== false) {
$options['isHTML'] = false;
echo "ToSendProcess: Set isHTML=false from content-type: " . $options['content-type'] . "\n";
} else {
// Fallback: detect HTML from content
$options['isHTML'] = (stripos($sMessage, '<html') !== false ||
stripos($sMessage, '<body') !== false ||
stripos($sMessage, '<div') !== false ||
stripos($sMessage, '<p') !== false);
echo "ToSendProcess: Detected isHTML from message content: " . ($options['isHTML'] ? 'true' : 'false') . "\n";
}
// Set content-type based on isHTML if not explicitly set
if (!isset($options['content-type']) || empty($options['content-type'])) {
$options['content-type'] = $options['isHTML'] ? 'text/html' : 'text/plain';
echo "ToSendProcess: Set content-type based on isHTML: " . $options['content-type'] . "\n";
}
// Priority: MessageFrom > Options
// Set from-name - Always use sender's name from the email being processed
// For bulk mails, the sender's name should be in Options (set during email processing)
if (!empty($fromName)) {
// Extract from MessageFrom if available
$options['from-name'] = trim($fromName, '\'"');
echo "ToSendProcess: SET from-name from MessageFrom: \"" . $options['from-name'] . "\"\n";
ErrorLog::logInfo('ToSendProcess: Set from-name from MessageFrom: "' . $options['from-name'] . '"');
} elseif (isset($options['from-name']) && !empty(trim($options['from-name']))) {
// Use from-name from Options (this should be the sender's name from the processed email)
$options['from-name'] = trim($options['from-name'], '\'"');
echo "ToSendProcess: Using from-name from Options (sender's name): \"" . $options['from-name'] . "\"\n";
ErrorLog::logInfo('ToSendProcess: Using from-name from Options (sender\'s name): "' . $options['from-name'] . '"');
} else {
// Try to extract from MessageFrom as last resort
if (!empty($rsM['MessageFrom'])) {
$messageFrom = trim($rsM['MessageFrom']);
if (strpos($messageFrom, '<') !== false && strpos($messageFrom, '>') !== false) {
$fromParts = explode('<', $messageFrom);
$extractedName = trim($fromParts[0], '\'"');
if (!empty($extractedName)) {
$options['from-name'] = $extractedName;
echo "ToSendProcess: Extracted from-name from MessageFrom as fallback: \"" . $options['from-name'] . "\"\n";
ErrorLog::logInfo('ToSendProcess: Extracted from-name from MessageFrom as fallback: "' . $options['from-name'] . '"');
}
}
}
}
// Final cleanup: ensure from-name is clean (no quotes) but don't set default
// For bulk mails, we want to use the sender's name, not a default
if (isset($options['from-name'])) {
$options['from-name'] = trim($options['from-name'], '\'"');
}
// Set from-address - MessageFrom > Options > extract from reply-to
if (!empty($fromAddress)) {
$options['from-address'] = trim($fromAddress);
// Also set reply-to to the same address
$options['reply-to-address'] = $options['from-address'];
ErrorLog::logInfo('ToSendProcess: Set from-address from MessageFrom: "' . $options['from-address'] . '"');
} elseif (isset($options['reply-to-address']) && !empty($options['reply-to-address'])) {
// Use reply-to as from-address if available
if (!isset($options['from-address']) || empty(trim($options['from-address']))) {
$options['from-address'] = trim($options['reply-to-address']);
ErrorLog::logInfo('ToSendProcess: Using reply-to-address as from-address: "' . $options['from-address'] . '"');
}
} elseif (isset($options['from-address'])) {
$options['from-address'] = trim($options['from-address']);
}
// Debug logging - show final options being sent
echo "ToSendProcess: FINAL OPTIONS - from-name: \"" . (isset($options['from-name']) ? $options['from-name'] : 'not set') . "\", from-address: \"" . (isset($options['from-address']) ? $options['from-address'] : 'not set') . "\", isHTML: " . ($options['isHTML'] ? 'true' : 'false') . ", content-type: " . (isset($options['content-type']) ? $options['content-type'] : 'not set') . "\n";
echo "ToSendProcess: Calling maill() to send to: " . $rsR["SendTo"] . "\n";
// Add extracted attachments to options
if (!empty($extractedAttachments)) {
if (!isset($options['attachments']) || !is_array($options['attachments'])) {
$options['attachments'] = [];
}
$options['attachments'] = array_merge($options['attachments'], $extractedAttachments);
ErrorLog::logInfo('ToSendProcess: Added ' . count($extractedAttachments) . ' extracted attachments to options');
}
// Add embedded images to options
if (!empty($embeddedImages)) {
$options['embedded_images'] = $embeddedImages;
echo "ToSendProcess: Added " . count($embeddedImages) . " embedded images to options\n";
foreach ($embeddedImages as $idx => $img) {
echo "ToSendProcess: Embedded image " . ($idx + 1) . " - CID: \"" . $img['cid'] . "\", Path: " . $img['path'] . ", Type: " . $img['content_type'] . "\n";
// Check if CID appears in HTML message
$cidInHtml = (stripos($sMessage, 'cid:' . $img['cid']) !== false || stripos($sMessage, '"cid:' . $img['cid'] . '"') !== false);
echo "ToSendProcess: CID \"" . $img['cid'] . "\" found in HTML: " . ($cidInHtml ? 'YES' : 'NO') . "\n";
}
ErrorLog::logInfo('ToSendProcess: Added ' . count($embeddedImages) . ' embedded images to options');
} else {
echo "ToSendProcess: No embedded images to add\n";
}
// Final cleanup: ensure from-name is clean (no quotes)
// For bulk mails, we want to use the sender's name from the email, not a default
if (isset($options['from-name'])) {
$options['from-name'] = trim($options['from-name'], '\'"');
}
// ErrorLog::logInfo('ToSendProcess: Final options being sent to maill() - from-name: "' . (isset($options['from-name']) ? $options['from-name'] : 'not set') . '", from-address: "' . (isset($options['from-address']) ? $options['from-address'] : 'not set') . '", isHTML: ' . ($options['isHTML'] ? 'true' : 'false') . ', content-type: ' . (isset($options['content-type']) ? $options['content-type'] : 'not set') . ', attachments: ' . (isset($options['attachments']) ? count($options['attachments']) : 0));
// ErrorLog::logInfo('ToSendProcess: Sending email to ' . $rsR["SendTo"] . ' with subject: ' . $rsM["Subject"]);
// ErrorLog::logInfo('ToSendProcess: Message preview: ' . substr($sMessage, 0, 200));
maill($rsR["SendTo"], $rsM["Subject"], $sMessage, null, $options);
echo "ToSendProcess: maill() call completed\n";
} else {
// Options parsing failed, use fallback with proper HTML detection and from-name
$fallbackOptions = array();
// Detect HTML from message content
$isHTML = (stripos($sMessage, '<html') !== false ||
stripos($sMessage, '<body') !== false ||
stripos($sMessage, '<div') !== false ||
stripos($sMessage, '<p') !== false);
$fallbackOptions['isHTML'] = $isHTML;
$fallbackOptions['content-type'] = $isHTML ? 'text/html' : 'text/plain';
// Use MessageFrom if available
if (!empty($fromName)) {
$fallbackOptions['from-name'] = trim($fromName, '\'"');
}
if (!empty($fromAddress)) {
$fallbackOptions['from-address'] = trim($fromAddress);
$fallbackOptions['reply-to-address'] = trim($fromAddress);
}
// Set default from-name to "CESA" if not determined
if (empty($fallbackOptions['from-name']) || trim($fallbackOptions['from-name']) === '') {
$fallbackOptions['from-name'] = 'CESA';
ErrorLog::logInfo('ToSendProcess: Set default from-name to "CESA" in fallback options');
}
// Add extracted attachments and embedded images to fallback options
if (!empty($extractedAttachments)) {
$fallbackOptions['attachments'] = $extractedAttachments;
}
if (!empty($embeddedImages)) {
$fallbackOptions['embedded_images'] = $embeddedImages;
}
// Final safety check: Ensure from-name is always set before calling maill()
$fallbackOptions['from-name'] = trim((string)($fallbackOptions['from-name'] ?? ''), '\'"');
if (empty($fallbackOptions['from-name'])) {
$fallbackOptions['from-name'] = 'CESA';
// ErrorLog::logInfo('ToSendProcess: Final safety check - Set from-name to "CESA" in fallback');
}
// ErrorLog::logInfo('ToSendProcess: Options parsing FAILED - using fallback with isHTML: ' . ($isHTML ? 'true' : 'false') . ', from-name: "' . (isset($fallbackOptions['from-name']) ? $fallbackOptions['from-name'] : 'not set') . '"');
// ErrorLog::logInfo('ToSendProcess: Options JSON that failed: ' . substr($rsM['Options'], 0, 500));
maill($rsR["SendTo"], $rsM["Subject"], $sMessage, null, $fallbackOptions);
}
} elseif ($rsM['Headers'] != '') {
// Use Headers field only if Options is not available
// Extract only relevant headers, not delivery headers
$cleanedHeaders = stripslashes($rsM['Headers']);
// Extract From, Reply-To, and Content-Type from headers
$sendHeaders = array();
// Extract From
if (preg_match('/^From:\s*(.+)$/mi', $cleanedHeaders, $matches)) {
$sendHeaders['from'] = trim($matches[1]);
}
// Extract Reply-To
if (preg_match('/^Reply-To:\s*(.+)$/mi', $cleanedHeaders, $matches)) {
$sendHeaders['reply-to'] = trim($matches[1]);
}
// Extract Content-Type to determine if HTML
$isHTML = false;
if (preg_match('/^Content-Type:\s*([^;]+)/mi', $cleanedHeaders, $matches)) {
$contentType = trim($matches[1]);
if (stripos($contentType, 'text/html') !== false) {
$isHTML = true;
}
}
// Build options from extracted headers
$options = array(
'isHTML' => $isHTML,
'content-type' => $isHTML ? 'text/html' : 'text/plain'
);
// Use MessageFrom field if available, otherwise use from headers
if (!empty($fromName)) {
$options['from-name'] = trim($fromName, '\'"');
}
if (!empty($fromAddress)) {
$options['from-address'] = $fromAddress;
$options['reply-to-address'] = $fromAddress;
} elseif (isset($sendHeaders['from'])) {
$options['from-address'] = $sendHeaders['from'];
}
if (isset($sendHeaders['reply-to'])) {
$options['reply-to-address'] = $sendHeaders['reply-to'];
}
// Clean from-name if set (remove quotes)
// For bulk mails, we want to use the sender's name from the email, not a default
if (isset($options['from-name'])) {
$options['from-name'] = trim($options['from-name'], '\'"');
}
// Add extracted attachments and embedded images
if (!empty($extractedAttachments)) {
if (!isset($options['attachments']) || !is_array($options['attachments'])) {
$options['attachments'] = [];
}
$options['attachments'] = array_merge($options['attachments'], $extractedAttachments);
}
if (!empty($embeddedImages)) {
$options['embedded_images'] = $embeddedImages;
}
// ErrorLog::logInfo('ToSendProcess: Sending email to ' . $rsR["SendTo"] . ' using extracted headers, isHTML: ' . ($isHTML ? 'true' : 'false'));
// ErrorLog::logInfo('ToSendProcess: Message preview: ' . substr($sMessage, 0, 200));
maill($rsR["SendTo"], $rsM["Subject"], $sMessage, null, $options);
} else {
// Default fallback - detect HTML from message content
$isHTML = (stripos($sMessage, '<html') !== false ||
stripos($sMessage, '<body') !== false ||
stripos($sMessage, '<div') !== false ||
stripos($sMessage, '<p') !== false);
$defaultOptions = array(
'isHTML' => $isHTML,
'content-type' => $isHTML ? 'text/html' : 'text/plain'
);
// Use MessageFrom field if available
if (!empty($fromName)) {
$defaultOptions['from-name'] = trim($fromName, '\'"');
}
if (!empty($fromAddress)) {
$defaultOptions['from-address'] = $fromAddress;
$defaultOptions['reply-to-address'] = $fromAddress;
}
// Clean from-name if set (remove quotes)
// For bulk mails, we want to use the sender's name from the email, not a default
if (isset($defaultOptions['from-name'])) {
$defaultOptions['from-name'] = trim($defaultOptions['from-name'], '\'"');
}
// Add extracted attachments and embedded images to default options
if (!empty($extractedAttachments)) {
$defaultOptions['attachments'] = $extractedAttachments;
}
if (!empty($embeddedImages)) {
$defaultOptions['embedded_images'] = $embeddedImages;
}
// ErrorLog::logInfo('ToSendProcess: Default options being sent to maill() - from-name: "' . (isset($defaultOptions['from-name']) ? $defaultOptions['from-name'] : 'not set') . '", from-address: "' . (isset($defaultOptions['from-address']) ? $defaultOptions['from-address'] : 'not set') . '", isHTML: ' . ($defaultOptions['isHTML'] ? 'true' : 'false') . ', content-type: ' . (isset($defaultOptions['content-type']) ? $defaultOptions['content-type'] : 'not set'));
// ErrorLog::logInfo('ToSendProcess: Sending email to ' . $rsR["SendTo"] . ' with subject: ' . $rsM["Subject"]);
// ErrorLog::logInfo('ToSendProcess: Message preview: ' . substr($sMessage, 0, 200));
maill($rsR["SendTo"], $rsM["Subject"], $sMessage, null, $defaultOptions);
}
$iMsgsSent++;
$bSomethingSent = true;
// Update ToSend record to mark as sent
if (isset($rsR["ToSendID"]) && !empty($rsR["ToSendID"])) {
$sql = "UPDATE ToSend SET Sent=1 WHERE ToSendID=" . intval($rsR["ToSendID"]);
$updateResult = mysqli_query($db, $sql);
if (!$updateResult) {
ErrorLog::logError('ToSendProcess: Failed to update ToSend record ToSendID=' . $rsR["ToSendID"] . ' - Error: ' . mysqli_error($db));
echo "ToSendProcess: ERROR - Failed to update ToSend record ToSendID=" . $rsR["ToSendID"] . "\n";
} else {
echo "ToSendProcess: Updated ToSend record ToSendID=" . $rsR["ToSendID"] . " to Sent=1\n";
}
} else {
ErrorLog::logError('ToSendProcess: ToSendID not found in result set for SendTo=' . $rsR["SendTo"]);
echo "ToSendProcess: ERROR - ToSendID not found for SendTo=" . $rsR["SendTo"] . "\n";
}
echo $rsR["SendTo"] . "<BR>\r\n";
echo ".";
flush();
ob_flush();
//wait for a quarter second
//usleep(100000);
}
mysqli_free_result($rsRec);
// Get school mailing list messages
/*
$sql = "SELECT sml_message_archive.subject AS Subject, sml_message_archive.message AS Message, sml_message_archive.id AS MessageID, sml_delivery_queue.email AS SendTo
FROM sml_message_archive, sml_delivery_queue
WHERE sml_message_archive.id=sml_delivery_queue.message_id
AND sml_message_archive.start_send<=UNIX_TIMESTAMP()";
$rsMsg = mysql_query($sql);
echo ".";
flush();
ob_flush();
while (($iMsgsSent <= $maxmsgs) && ($rsM = mysql_fetch_assoc($rsMsg))) {
$bSendFinished = false;
$sql = "DELETE FROM sml_delivery_queue WHERE message_id=".$rsM["MessageID"]." AND email='".$rsM["SendTo"]."'";
mysql_query($sql);
mail(stripslashes($rsM["SendTo"]), $rsM["Subject"], $rsM["Message"], "MIME-Version: 1.0" . "\r\n"."Content-type:text/html;charset=UTF-8" . "\r\n"."From: brenda@cesa.co.za"."\r\n"."Reply-To: brenda@cesa.co.za"."\r\n");
$iMsgsSent++;
$bSomethingSent = true;
$sql = "UPDATE sml_message_archive SET recipients=recipients+1 WHERE id=".$rsM["MessageID"];
mysql_query($sql);
echo $rsM["SendTo"]."<BR>";
echo ".";
flush();
ob_flush();
usleep(100000);
}
*/
//if ($bSomethingSent) {
$sql = "UPDATE LastSend SET EndSend='" . date("Y-m-d H:i:s") . "'";
mysqli_query($db, $sql);
}
//}
} else {
echo "Can't send yet.";
}
$sql = "DELETE FROM ToSend WHERE Sent=1 AND MessageID IN "
. "(SELECT MessageID FROM Messages WHERE DateSent <= DATE_SUB(NOW(), INTERVAL 21 DAY))";
mysqli_query($db, $sql);
$sql = "SELECT Messages.MessageID, (
SELECT COUNT(ToSend.ToSendID) AS Num FROM ToSend WHERE ToSend.MessageID = Messages.MessageID AND ToSend.Sent = 0) AS Num
FROM Messages
WHERE SendComplete=0 GROUP BY Messages.MessageID;";
echo $sql . "\r\n";
$rsRec = mysqli_query($db, $sql);
while ($rsRow = mysqli_fetch_assoc($rsRec)) {
if ($rsRow["Num"] == 0) {
$sql = "UPDATE Messages SET SendComplete=1, DateComplete='" . date("Y-m-d H:i:s") . "' WHERE MessageID='" . $rsRow["MessageID"] . "'";
echo $sql . "\r\n";
mysqli_query($db, $sql);
}
}
flush();
ob_flush();
} catch (Exception $ex) {
unlink($lockFile);
maill('cesa-bulk-mail@igotafrica.com', 'CESA Mailing List Error', 'Sending messages failed: ' . $ex->getMessage() . ' - ' . $ex->getTraceAsString());
}
if (isset($db) && is_object($db)) {
mysqli_close($db);
}
if (isset($conni) && is_object($conni)) {
mysqli_close($conni);
}
unlink($lockFile);