Your IP : 216.73.217.117


Current Path : /var/www/cesa.co.za/crons/
Upload File :
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);