Linux cesa-www-main 6.1.0-49-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64
Apache/2.4.68 (Debian)
Server IP : 10.218.0.2 & Your IP : 216.73.216.28
Domains :
Cant Read [ /etc/named.conf ]
User : www-data
Terminal
Auto Root
Create File
Create Folder
Localroot Suggester
Backdoor Destroyer
Readme
/
var /
www /
cesa.co.za /
php /
includes /
mail /
Delete
Unzip
Name
Size
Permission
Date
Action
MailMessages.php
131.92
KB
-rw-r--r--
2026-08-14 12:05
Save
Rename
<?php /** * Class to manage the Message stable in the mail database */ class MailMessages extends BaseTableClass { /** * Get the mail box configurations * @return array */ public static function getMailBoxConfigs () { return [ 'bce' => [ 'host' => 'mail.cesasce.co.za', 'port' => 993, 'username' => 'cesa-bce@cesasce.co.za', 'password' => 'n58LgWtwmdQ6K75', 'audience-select' => "SELECT CCEStudents.`EmailAddress` AS Email, CCEStudents.`EmailAddress2` AS Email2, CCEStudents.Organisation As OfficeName, `Title` as ContactDesignation, CCEStudents.KnownAs as ContactInitials, CCEStudents.Surname AS ContactSurname, 0 AS FirmID from CCEStudents JOIN CCEStudentCourses using (CCEStudentID) JOIN CCESetup ON (CCEStudentCourses.`CCECourseScheduleID`.CCESetup.`CurrentScheduleID`) where Accepted=1", ], 'hr-forum' => [ 'host' => 'mail.cesasce.co.za', 'port' => 993, 'username' => 'cesa-hrforum@cesasce.co.za', 'password' => 'n58LgWtwmdQ6K75', 'audience-select' => "SELECT DISTINCT Email, '' AS Email2, Company as OfficeName, '' as ContactDesignation, " . "FirstName as ContactInitials, Surname as ContactSurname, 0 AS FirmID " . "FROM HRForum WHERE Email IS NOT NULL AND Email<>'' AND Subscribed=1 " . "UNION " . "SELECT 'cesa-hr-forum@igotafrica.com' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Mr' AS ContactDesignation, 'O' AS ContactInitials, 'Saayman' AS ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'brenda@cesa.co.za' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Mrs' AS ContactDesignation, 'B' AS ContactInitials, 'Lacey-Smith' AS ContactSurname, " . "0 AS FirmID " . "UNION " . "SELECT 'oliver@cesa.co.za' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Mr' AS ContactDesignation, 'O' AS ContactInitials, 'Rowe' AS ContactSurname, 0 AS FirmID", ], 'mandated-principals' => [ 'host' => 'mail.cesasce.co.za', 'port' => 993, 'username' => 'cesa-mandated-principals@cesasce.co.za', 'password' => 'n58LgWtwmdQ6K75', 'audience-select' => "SELECT DISTINCT TRIM(MC.Email) as Email, TRIM(MC.Email2) as Email2, OfficeName, " . "ContactDesignation, ContactInitials, ContactSurname, MF.FirmID " . "FROM tblMemberFirms MF " . "INNER JOIN (tblMemberFirmOffices MFO " . "INNER JOIN MemberContacts MC ON MFO.OfficeID = MC.OfficeID) " . "ON MF.FirmID = MFO.FirmID " . "WHERE MC.Email Is Not Null AND MC.Mail = 0 AND " . "TRIM(MC.Email) <> '' and " . "(MC.Email like '%@%.%') and " . "(MC.MandatedPrincipal=1) and " . "((MF.DeclarationReceived > '2005-12-31' AND LOWER(MF.MemberStatus) = 'member') OR " . "(LOWER(MF.MemberStatus) = 'retired member') Or " . "(LOWER(MF.MemberStatus) = 'honorary member')) " . "UNION " . "SELECT 'chrisc@cesa.co.za' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Mr' AS ContactDesignation, 'C' AS ContactInitials, 'Campbell' AS ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'oliver@cesa.co.za' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Mr' AS ContactDesignation, 'O' AS ContactInitials, 'Rowe' AS ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'bonolo@cesa.co.za' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Ms' AS ContactDesignation, 'B' AS ContactInitials, 'Nkgodi' AS ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'cpdverification@cesa.co.za.co.za' AS Email, NULL AS Email2, 'CPD Verification' AS OfficeName, " . "'Ms' AS ContactDesignation, 'S' AS ContactInitials, 'Moseley Walker' AS ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'cesa-mandated-principals@igotafrica.com' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Mr' AS ContactDesignation, 'O' AS ContactInitials, 'Saayman' AS ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'francis.gibbons@outlook.com' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Mr' AS ContactDesignation, 'F' AS ContactInitials, 'Gibbons' AS ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'finance@cesa.co.za' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Mr' AS ContactDesignation, 'F' AS ContactInitials, 'Finance' AS ContactSurname, 0 AS FirmID", ], 'mandated-principals-office-heads' => [ 'host' => 'mail.cesasce.co.za', 'port' => 993, 'username' => 'cesa-mandated-principals-office-heads@cesasce.co.za', 'password' => 'n58LgWtwmdQ6K75', 'audience-select' => "SELECT DISTINCT TRIM(MC.Email) as Email, TRIM(MC.Email2) as Email2, OfficeName, " . "ContactDesignation, ContactInitials, ContactSurname, MF.FirmID " . "FROM tblMemberFirms MF " . "INNER JOIN (tblMemberFirmOffices MFO " . "INNER JOIN MemberContacts MC ON MFO.OfficeID = MC.OfficeID) " . "ON MF.FirmID = MFO.FirmID " . "WHERE MC.Email Is Not Null AND MC.Mail = 0 AND " . "TRIM(MC.Email) <> '' and " . "(MC.Email like '%@%.%') and " . "((MC.OfficeHead=1) OR (MC.MandatedPrincipal=1)) and " . "((MF.DeclarationReceived > '2007-12-31' AND LOWER(MF.MemberStatus) = 'member') OR " . "(LOWER(MF.MemberStatus) = 'retired member') Or " . "(LOWER(MF.MemberStatus) = 'honorary member')) " . "UNION " . "SELECT 'chrisc@cesa.co.za' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Mr' AS ContactDesignation, 'C' AS ContactInitials, 'Campbell' AS ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'cpdverification@cesa.co.za.co.za' AS Email, NULL AS Email2, 'CPD Verification' AS OfficeName, " . "'Ms' AS ContactDesignation, 'S' AS ContactInitials, 'Moseley Walker' AS ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'oliver@cesa.co.za' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Mr' AS ContactDesignation, 'O' AS ContactInitials, 'Rowe' AS ContactSurname, 0 AS FirmID", ], 'marketing' => [ 'host' => 'mail.cesasce.co.za', 'port' => 993, 'username' => 'cesa-marketing@cesasce.co.za', 'password' => 'n58LgWtwmdQ6K75', 'audience-select' => "SELECT DISTINCT MarketingEmail as Email, '' as Email2, FirmName As OfficeName, " . "'Mr' as ContactDesignation, '' as ContactInitials, MarketingContact AS ContactSurname, MF.FirmID " . "FROM tblMemberFirms MF " . "WHERE MarketingEmail Is Not Null AND " . "TRIM(MarketingEmail) <> '' and (MarketingEmail like '%@%.%') and " . "((MF.DeclarationReceived > '2007-12-31' AND LOWER(MF.MemberStatus) = 'member') OR " . "(LOWER(MF.MemberStatus) = 'retired member') Or " . "(LOWER(MF.MemberStatus) = 'honorary member'))", ], 'members' => [ 'host' => 'mail.cesasce.co.za', 'port' => 993, 'username' => 'cesa-members@cesasce.co.za', 'password' => 'n58LgWtwmdQ6K75', 'audience-select' => "SELECT DISTINCT TRIM(MC.Email) as Email, TRIM(MC.Email2) as Email2, " . "OfficeName, ContactDesignation, ContactInitials, ContactSurname, MF.FirmID " . "FROM tblMemberFirms MF " . "INNER JOIN (tblMemberFirmOffices MFO " . "INNER JOIN MemberContacts MC ON MFO.OfficeID = MC.OfficeID) " . "ON MF.FirmID = MFO.FirmID " . "WHERE MC.Email Is Not Null AND MC.Mail = 0 AND " . "TRIM(MC.Email) <> '' and " . "(MC.Email like '%@%.%') and " . "((MF.DeclarationReceived > '2007-12-31' AND MF.MemberStatus = 'member') OR " . "(MF.MemberStatus = 'retired member') Or " . "(MF.MemberStatus = 'honorary member')) " . "UNION " . "SELECT DISTINCT MarketingEmail as Email, '' as Email2, FirmName As OfficeName, " . "'' as ContactDesignation, '' as ContactInitials, MarketingContact AS ContactSurname, MF.FirmID " . "FROM tblMemberFirms MF " . "WHERE MarketingEmail Is Not Null AND " . "TRIM(MarketingEmail) <> '' and (MarketingEmail like '%@%.%') and " . "((MF.DeclarationReceived > '2007-12-31' AND LOWER(MF.MemberStatus) = 'member') OR " . "(LOWER(MF.MemberStatus) = 'retired member') Or " . "(LOWER(MF.MemberStatus) = 'honorary member')) " . "UNION " . "SELECT DISTINCT HRTrainingEmail as Email, '' as Email2, FirmName As OfficeName, " . "'' as ContactDesignation, '' as ContactInitials, HRTrainingContact AS ContactSurname, MF.FirmID " . "FROM tblMemberFirms MF " . "WHERE HRTrainingEmail Is Not Null AND " . "TRIM(HRTrainingEmail) <> '' and (HRTrainingEmail like '%@%.%') and " . "((MF.DeclarationReceived > '2007-12-31' AND LOWER(MF.MemberStatus) = 'member') OR " . "(LOWER(MF.MemberStatus) = 'retired member') Or " . "(LOWER(MF.MemberStatus) = 'honorary member')) " . "UNION " . "SELECT DISTINCT YPFEmail as Email, '' as Email2, FirmName As OfficeName, " . "'' as ContactDesignation, '' as ContactInitials, YPFContact AS ContactSurname, MF.FirmID " . "FROM tblMemberFirms MF " . "WHERE YPFEmail Is Not Null AND " . "TRIM(YPFEmail) <> '' and (YPFEmail like '%@%.%') and " . "((MF.DeclarationReceived > '2007-12-31' AND LOWER(MF.MemberStatus) = 'member') OR " . "(LOWER(MF.MemberStatus) = 'retired member') Or " . "(LOWER(MF.MemberStatus) = 'honorary member')) " . "UNION " . "SELECT DISTINCT QMSChampEmail as Email, '' as Email2, FirmName As OfficeName, " . "'' as ContactDesignation, '' as ContactInitials, QMSChampion AS ContactSurname, MF.FirmID " . "FROM tblMemberFirms MF " . "WHERE QMSChampEmail Is Not Null AND " . "TRIM(QMSChampEmail) <> '' and (QMSChampEmail like '%@%.%') and " . "((MF.DeclarationReceived > '2007-12-31' AND LOWER(MF.MemberStatus) = 'member') OR " . "(LOWER(MF.MemberStatus) = 'retired member') Or " . "(LOWER(MF.MemberStatus) = 'honorary member')) " . "UNION " . "SELECT DISTINCT SustainabilityChampEmail as Email, '' as Email2, FirmName As OfficeName, " . "'' as ContactDesignation, '' as ContactInitials, SustainabilityChampion AS ContactSurname, MF.FirmID " . "FROM tblMemberFirms MF " . "WHERE SustainabilityChampEmail Is Not Null AND " . "TRIM(SustainabilityChampEmail) <> '' and (SustainabilityChampEmail like '%@%.%') and " . "((MF.DeclarationReceived > '2007-12-31' AND LOWER(MF.MemberStatus) = 'member') OR " . "(LOWER(MF.MemberStatus) = 'retired member') Or " . "(LOWER(MF.MemberStatus) = 'honorary member')) " . "UNION " . "SELECT DISTINCT ProcurementEmail as Email, '' as Email2, FirmName As OfficeName, " . "'' as ContactDesignation, '' as ContactInitials, ProcurementContact AS ContactSurname, MF.FirmID " . "FROM tblMemberFirms MF " . "WHERE ProcurementEmail Is Not Null AND " . "TRIM(ProcurementEmail) <> '' and (ProcurementEmail like '%@%.%') and " . "((MF.DeclarationReceived > '2007-12-31' AND LOWER(MF.MemberStatus) = 'member') OR " . "(LOWER(MF.MemberStatus) = 'retired member') Or " . "(LOWER(MF.MemberStatus) = 'honorary member')) " . "UNION " . "SELECT 'chrisc@cesa.co.za' AS Email, '' as Email2, 'CESA' as OfficeName, " . "'' as ContactDesignation, 'C' as ContactInitials, 'Campbell' as ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'oliver@cesa.co.za' AS Email, '' as Email2, 'CESA' as OfficeName, " . "'' as ContactDesignation, 'O' as ContactInitials, " . "'Rowe' as ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'bonolo@cesa.co.za' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Ms' AS ContactDesignation, 'B' AS ContactInitials, " . "'Nkgodi' AS ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'cpdverification@cesa.co.za.co.za' AS Email, NULL AS Email2, 'CPD Verification' AS OfficeName, " . "'Ms' AS ContactDesignation, 'S' AS ContactInitials, 'Moseley Walker' AS ContactSurname, 0 AS FirmID", ], 'mprin-oheads-marketing' => [ 'host' => 'mail.cesasce.co.za', 'port' => 993, 'username' => 'cesa-mprin-oheads-marketing@cesasce.co.za', 'password' => 'n58LgWtwmdQ6K75', 'audience-select' => "SELECT TRIM(MC.Email) AS Email, TRIM(MC.Email2) AS Email2, OfficeName, " . "ContactDesignation, ContactInitials, ContactSurname, MF.FirmID " . "FROM tblMemberFirms MF " . "INNER JOIN (tblMemberFirmOffices MFO " . "INNER JOIN MemberContacts MC ON MFO.OfficeID = MC.OfficeID) " . "ON MF.FirmID = MFO.FirmID " . "WHERE MC.Email IS NOT NULL AND MC.Mail = 0 AND " . "TRIM(MC.Email) <> '' AND " . "(MC.Email LIKE '%@%.%') AND " . "((MC.OfficeHead=1) OR (MC.MandatedPrincipal=1)) AND " . "((MF.DeclarationReceived > '2007-12-31' AND LOWER(MF.MemberStatus) = 'member') OR " . "(LOWER(MF.MemberStatus) = 'retired member') OR " . "(LOWER(MF.MemberStatus) = 'honorary member')) " . "UNION " . "SELECT MarketingEmail, '', FirmName, '', '', MarketingContact, FirmID " . "FROM tblMemberFirms MF " . "WHERE TRIM(MarketingEmail) <> '' AND " . "(MarketingEmail LIKE '%@%.%') AND " . "((MF.DeclarationReceived > '2007-12-31' AND LOWER(MF.MemberStatus) = 'member') OR " . "(LOWER(MF.MemberStatus) = 'retired member') OR " . "(LOWER(MF.MemberStatus) = 'honorary member'))", ], 'office-heads' => [ 'host' => 'mail.cesasce.co.za', 'port' => 993, 'username' => 'cesa-office-heads@cesasce.co.za', 'password' => 'n58LgWtwmdQ6K75', 'audience-select' => "SELECT DISTINCT TRIM(MC.Email) as Email, TRIM(MC.Email2) as Email2, OfficeName, " . "ContactDesignation, ContactInitials, ContactSurname, MF.FirmID " . "FROM tblMemberFirms MF " . "INNER JOIN (tblMemberFirmOffices MFO " . "INNER JOIN MemberContacts MC ON MFO.OfficeID = MC.OfficeID) " . "ON MF.FirmID = MFO.FirmID " . "WHERE MC.Email Is Not Null AND MC.Mail = 0 AND " . "TRIM(MC.Email) <> '' and " . "(MC.Email like '%@%.%') and " . "(MC.OfficeHead=1) and " . "((MF.DeclarationReceived > '2007-12-31' AND LOWER(MF.MemberStatus) = 'member') OR " . "(LOWER(MF.MemberStatus) = 'retired member') Or " . "(LOWER(MF.MemberStatus) = 'honorary member')) " . "UNION " . "SELECT 'chrisc@cesa.co.za' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Mr' AS ContactDesignation, 'C' AS ContactInitials, 'Campbell' AS ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'oliver@cesa.co.za' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Mr' AS ContactDesignation, 'O' AS ContactInitials, " . "'Rowe' AS ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'cpdverification@cesa.co.za.co.za' AS Email, NULL AS Email2, 'CPD Verification' AS OfficeName, " . "'Ms' AS ContactDesignation, 'S' AS ContactInitials, 'Moseley Walker' AS ContactSurname, 0 AS FirmID", ], 'principals' => [ 'host' => 'mail.cesasce.co.za', 'port' => 993, 'username' => 'cesa-principals@cesasce.co.za', 'password' => 'n58LgWtwmdQ6K75', 'audience-select' => "SELECT DISTINCT TRIM(MC.Email) as Email, TRIM(MC.Email2) as Email2, OfficeName, " . "ContactDesignation, ContactInitials, ContactSurname, MF.FirmID " . "FROM tblMemberFirms MF " . "INNER JOIN (tblMemberFirmOffices MFO " . "INNER JOIN MemberContacts MC ON MFO.OfficeID = MC.OfficeID) " . "ON MF.FirmID = MFO.FirmID " . "WHERE MC.Email Is Not Null AND MC.Mail = 0 AND " . "TRIM(MC.Email) <> '' and (MC.Email like '%@%.%') and " . "(MC.Principal=1) and " . "((MF.DeclarationReceived > '2007-12-31' AND LOWER(MF.MemberStatus) = 'member') OR " . "(LOWER(MF.MemberStatus) = 'retired member') Or " . "(LOWER(MF.MemberStatus) = 'honorary member')) " . "UNION " . "SELECT 'chrisc@cesa.co.za' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Mr' AS ContactDesignation, 'C' AS ContactInitials, 'Campbell' AS ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'oliver@cesa.co.za' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Mr' AS ContactDesignation, 'O' AS ContactInitials, 'Rowe' AS ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'bonolo@cesa.co.za' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Ms' AS ContactDesignation, 'B' AS ContactInitials, 'Nkgodi' AS ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'diane.liebenberg@dcx7.co.za' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Ms' AS ContactDesignation, 'D' AS ContactInitials, 'Liebenberg' AS ContactSurname, 0 AS FirmID", ], 'principals-office-heads' => [ 'host' => 'mail.cesasce.co.za', 'port' => 993, 'username' => 'cesa-principals-office-heads@cesasce.co.za', 'password' => 'n58LgWtwmdQ6K75', 'audience-select' => "SELECT DISTINCT TRIM(MC.Email) as Email, TRIM(MC.Email2) as Email2, OfficeName, " . "ContactDesignation, ContactInitials, ContactSurname, MF.FirmID " . "FROM tblMemberFirms MF " . "INNER JOIN (tblMemberFirmOffices MFO " . "INNER JOIN MemberContacts MC ON MFO.OfficeID = MC.OfficeID) " . "ON MF.FirmID = MFO.FirmID " . "WHERE MC.Email Is Not Null AND MC.Mail = 0 AND " . "TRIM(MC.Email) <> '' and " . "(MC.Email like '%@%.%') and " . "((MC.OfficeHead=1) OR (MC.Principal=1)) and " . "((MF.DeclarationReceived > '2007-12-31' AND LOWER(MF.MemberStatus) = 'member') OR " . "(LOWER(MF.MemberStatus) = 'retired member') Or " . "(LOWER(MF.MemberStatus) = 'honorary member')) " . "UNION " . "SELECT 'chrisc@cesa.co.za' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Mr' AS ContactDesignation, 'C' AS ContactInitials, 'Campbell' AS ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'oliver@cesa.co.za' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Rowe' AS ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'cpdverification@cesa.co.za.co.za' AS Email, NULL AS Email2, 'CPD Verification' AS OfficeName, " . "'Ms' AS ContactDesignation, 'S' AS ContactInitials, 'Moseley Walker' AS ContactSurname, 0 AS FirmID " . "UNION " . "SELECT 'cesa-office-heads@igotafrica.com' AS Email, NULL AS Email2, 'CESA' AS OfficeName, " . "'Mr' AS ContactDesignation, 'O' AS ContactInitials, 'Saayman' AS ContactSurname, 0 AS FirmID", ], // TODO: If mail is sent to this address, store attachments in uploads and send link. // 'upload' => [ // 'host' => 'mail.cesasce.co.za', // 'port' => 993, // 'username' => 'cesa-upload@cesasce.co.za', // 'password' => 'n58LgWtwmdQ6K75', // 'audience-select' => "SELECT 'cesa@igotafrica.com' AS Email, 'Only Office' AS OfficeName, " // . "'Mr' AS ContactDesignation, 'O' AS ContactInitials, 'Saayman' AS ContactSurname", // ], 'ypf' => [ 'host' => 'mail.cesasce.co.za', 'port' => 993, 'username' => 'cesa-ypf@cesasce.co.za', 'password' => 'n58LgWtwmdQ6K75', 'audience-select' => "SELECT DISTINCT Email, '' AS OfficeName, '' AS ContactDesignation, " . "'' AS ContactInitials, '' AS ContactSurname, 0 AS FirmID FROM ( " . "SELECT TRIM(MC.Email) AS Email " . "FROM tblMemberFirms MF " . "INNER JOIN (tblMemberFirmOffices MFO " . "INNER JOIN MemberContacts MC ON MFO.OfficeID = MC.OfficeID) " . "ON MF.FirmID = MFO.FirmID " . "WHERE MC.Email IS NOT NULL AND MC.Mail = 0 AND " . "TRIM(MC.Email) <> '' AND " . "(MC.Email LIKE '%@%.%') AND " . "(MC.YPF='y') AND " . "((MF.DeclarationReceived > '2005-12-31' AND LOWER(MF.MemberStatus) = 'member') OR " . "(LOWER(MF.MemberStatus) = 'retired member') OR " . "(LOWER(MF.MemberStatus) = 'honorary member')) " . "UNION " . "SELECT TRIM(MC.Email2) AS Email " . "FROM tblMemberFirms MF " . "INNER JOIN (tblMemberFirmOffices MFO " . "INNER JOIN MemberContacts MC ON MFO.OfficeID = MC.OfficeID) " . "ON MF.FirmID = MFO.FirmID " . "WHERE MC.Email2 IS NOT NULL AND MC.Mail = 0 AND " . "TRIM(MC.Email2) <> '' AND " . "(MC.Email2 LIKE '%@%.%') AND " . "(MC.YPF='y') AND " . "((MF.DeclarationReceived > '2005-12-31' AND LOWER(MF.MemberStatus) = 'member') OR " . "(LOWER(MF.MemberStatus) = 'retired member') OR " . "(LOWER(MF.MemberStatus) = 'honorary member')) " . "UNION " . "SELECT PreferredEmail AS Email " . "FROM YPFContacts " . "WHERE PreferredEmail IS NOT NULL AND TRIM(PreferredEmail) <> '' AND " . "(PreferredEmail LIKE '%@%.%') " . ") Tmp", ], 'ypf-members' => [ 'host' => 'mail.cesasce.co.za', 'port' => 993, 'username' => 'cesa-ypf-members@cesasce.co.za', 'password' => 'n58LgWtwmdQ6K75', 'audience-select' => "SELECT TRIM(MC.Email) as Email, TRIM(MC.Email2) as Email2, OfficeName, " . "ContactDesignation, ContactInitials, ContactSurname, MF.FirmID " . "FROM tblMemberFirms MF " . "INNER JOIN (tblMemberFirmOffices MFO " . "INNER JOIN MemberContacts MC ON MFO.OfficeID = MC.OfficeID) " . "ON MF.FirmID = MFO.FirmID " . "WHERE MC.Email Is Not Null AND MC.Mail = 0 AND " . "TRIM(MC.Email) <> '' and " . "(MC.Email like '%@%.%') and " . "(MC.YPF='y') and " . "((MF.DeclarationReceived > '2005-12-31' AND LOWER(MF.MemberStatus) = 'member') OR " . "(LOWER(MF.MemberStatus) = 'retired member') Or " . "(LOWER(MF.MemberStatus) = 'honorary member'))", ], 'ypf-other' => [ 'host' => 'mail.cesasce.co.za', 'port' => 993, 'username' => 'cesa-ypf-other@cesasce.co.za', 'password' => 'n58LgWtwmdQ6K75', 'audience-select' => "SELECT DISTINCT PreferredEmail AS Email, '' AS Email2, Association AS OfficeName, " . "ContactDesignation, ContactInitials, ContactSurname, FirmID " . "FROM YPFContacts " . "WHERE PreferredEmail IS NOT NULL AND TRIM(PreferredEmail) <> '' AND " . "(PreferredEmail LIKE '%@%.%') AND FirmID IS NULL", ], 'cesa-test' => [ 'host' => 'mail.cesasce.co.za', 'port' => 993, 'username' => 'cesa-test@cesasce.co.za', 'password' => 'n58LgWtwmdQ6K75', 'audience-select' => "SELECT 'cesa@igotafrica.com' AS Email, 'Only Office' AS OfficeName, " . "'Mr' AS ContactDesignation, 'O' AS ContactInitials, 'Saayman' AS ContactSurname " . "UNION ALL " . "SELECT 'gillian@cesa.co.za' AS Email, 'CESA Office' AS OfficeName, 'Ms' AS ContactDesignation, " . "'G' AS ContactInitials, 'Wands' AS ContactSurname " . "UNION ALL " . "SELECT 'brenda@cesa.co.za' AS Email, 'CESA Office' AS OfficeName, 'Ms' AS ContactDesignation, " . "'G' AS ContactInitials, 'Wands' AS ContactSurname", ] ]; } /** * Split the message into parts using mailparse extension * @param string $message * @param bool $debug Enable debug logging * @return array */ public static function splitMessageParts($message, $debug = false) { $debugMessages = []; $parts = []; $parts['from'] = ""; $parts['subject'] = ""; $parts['header'] = ""; $parts['message'] = ""; $parts['headers'] = array(); $parts['html_message'] = ""; $parts['text_message'] = ""; $parts['multipart_message'] = ""; $parts['attachments'] = []; // Check if mailparse extension is available if (!extension_loaded('mailparse')) { // // ErrorLog::logInfo('MailMessages: mailparse extension not available, falling back to custom parsing'); return self::splitMessagePartsLegacy($message, $debug); } try { // Parse raw email using mailparse $parsed = self::parseRawEmail($message); // Map parsed data to expected format $parts['from'] = $parsed['from']; $parts['subject'] = $parsed['subject']; $parts['headers'] = $parsed['headers']; $parts['html_message'] = $parsed['html']; $parts['text_message'] = $parsed['text']; $parts['attachments'] = $parsed['attachments']; if ($debug) { $debugMessages[] = "MailMessages: Parsed with mailparse - HTML: " . strlen($parsed['html']) . ", Text: " . strlen($parsed['text']); $debugMessages[] = "MailMessages: Found " . count($parsed['attachments']) . " attachments"; } } catch (Exception $e) { // // ErrorLog::logInfo('MailMessages: mailparse error: ' . $e->getMessage() . ', falling back to legacy parsing'); return self::splitMessagePartsLegacy($message, $debug); } // Clean and encode all message parts $parts = self::cleanMessageParts($parts); // Log debug messages if enabled if ($debug && !empty($debugMessages)) { $debugLog = "MailMessages Debug Log:\n" . implode("\n", $debugMessages); // ErrorLog::logInfo($debugLog); } return $parts; } /** * Legacy message parsing method (fallback when mailparse is not available) * @param string $message * @param bool $debug Enable debug logging * @return array */ private static function splitMessagePartsLegacy($message, $debug = false) { $debugMessages = []; $parts = []; $parts['from'] = ""; $parts['subject'] = ""; $parts['header'] = ""; $parts['message'] = ""; $parts['headers'] = array(); $parts['html_message'] = ""; $parts['text_message'] = ""; $parts['multipart_message'] = ""; $parts['attachments'] = []; // Split message into headers and body $headerEnd = strpos($message, "\r\n\r\n"); if ($headerEnd === false) { $headerEnd = strpos($message, "\n\n"); } if ($headerEnd !== false) { $headers = substr($message, 0, $headerEnd); $body = substr($message, $headerEnd + 2); } else { $headers = $message; $body = ""; } // Parse headers $headerLines = explode("\n", $headers); $currentHeader = ""; $currentValue = ""; foreach ($headerLines as $line) { $line = trim($line); if (empty($line)) continue; // Check if this is a continuation line (starts with space or tab) if (preg_match('/^\s/', $line)) { // This is a continuation of the previous header $currentValue .= " " . trim($line); } else { // Save previous header if exists if (!empty($currentHeader)) { $parts['headers'][$currentHeader] = $currentValue; } // Start new header $colonPos = strpos($line, ':'); if ($colonPos !== false) { $currentHeader = trim(substr($line, 0, $colonPos)); $currentValue = trim(substr($line, $colonPos + 1)); } } } // Save the last header if (!empty($currentHeader)) { $parts['headers'][$currentHeader] = $currentValue; } // Set basic fields $parts['from'] = isset($parts['headers']['From']) ? $parts['headers']['From'] : ''; $parts['subject'] = isset($parts['headers']['Subject']) ? $parts['headers']['Subject'] : ''; // Debug: Log what we found in headers if (isset($parts['headers']['Subject'])) { $debugMessages[] = "MailMessages: Found Subject header: " . $parts['headers']['Subject']; } else { $debugMessages[] = "MailMessages: No Subject header found"; } if (isset($parts['headers']['From'])) { $debugMessages[] = "MailMessages: Found From header: " . $parts['headers']['From']; } else { $debugMessages[] = "MailMessages: No From header found"; } // Check if this is a multipart message by looking for boundaries in the body $boundaries = self::extractAllBoundaries($body); $boundary = !empty($boundaries) ? $boundaries[0] : ''; if ($debug) { $debugMessages[] = "MailMessages: Extracted boundaries: " . implode(', ', $boundaries); $debugMessages[] = "MailMessages: Primary boundary: " . ($boundary ?: 'none'); $debugMessages[] = "MailMessages: Body length: " . strlen($body); $debugMessages[] = "MailMessages: First 200 chars of body: " . substr($body, 0, 200); } if (!empty($boundary)) { // Handle multipart message with all boundaries $parts = self::parseComplexMultipartMessage($body, $boundaries, $parts, $debug, $debugMessages); } else { // Handle simple message $parts = self::parseSimpleMessage($body, $parts, $debug, $debugMessages); } // Handle truncated messages $parts = self::handleTruncatedMessage($body, $parts, $debug, $debugMessages); // If we still don't have content, try to extract it using the special methods if (empty($parts['text_message']) && empty($parts['html_message'])) { // First try the original message format handler $extractedContent = self::handleOriginalMessageFormat($body); if (!empty($extractedContent['text'])) { $parts['text_message'] = $extractedContent['text']; if (!empty($extractedContent['subject']) && empty($parts['subject'])) { $parts['subject'] = $extractedContent['subject']; } if (!empty($extractedContent['from']) && empty($parts['from'])) { $parts['from'] = $extractedContent['from']; } } else { // Fallback to the general content extractor $extractedContent = self::extractEmailContent($body); if (!empty($extractedContent['text'])) { $parts['text_message'] = $extractedContent['text']; } } } // Final attempt to extract HTML content if we still don't have it if (empty($parts['html_message'])) { $htmlContent = self::extractHtmlFromRawBody($body); if (!empty($htmlContent)) { $parts['html_message'] = $htmlContent; if ($debug) { $debugMessages[] = "MailMessages: Final HTML extraction successful, length: " . strlen($htmlContent); } } } // Clean and encode all message parts $parts = self::cleanMessageParts($parts); // Log debug messages if enabled if ($debug && !empty($debugMessages)) { $debugLog = "MailMessages Debug Log:\n" . implode("\n", $debugMessages); // ErrorLog::logInfo($debugLog); } return $parts; } /** * Parse a raw email string using mailparse * @param string $rawEmail * @return array */ public static function parseRawEmail($rawEmail) { $parts = [ 'headers' => [], 'from' => '', 'to' => '', 'reply_to' => '', 'subject' => '', 'date' => '', 'html' => '', 'text' => '', 'attachments' => [], 'charset' => 'UTF-8', 'content_type' => 'text/plain' ]; $mime = mailparse_msg_create(); mailparse_msg_parse($mime, $rawEmail); $structure = mailparse_msg_get_structure($mime); foreach ($structure as $section) { $part = mailparse_msg_get_part($mime, $section); $sectionData = mailparse_msg_get_part_data($part); // Store headers from main section if (empty($parts['headers']) && isset($sectionData['headers'])) { foreach ($sectionData['headers'] as $k => $v) { // Handle array values (multiple headers with same name) if (is_array($v)) { $parts['headers'][$k] = $v; } else { $parts['headers'][$k] = $v; } } // Extract basic fields, handling arrays $parts['from'] = is_array($sectionData['headers']['from'] ?? '') ? implode(', ', $sectionData['headers']['from']) : ($sectionData['headers']['from'] ?? ''); $parts['to'] = is_array($sectionData['headers']['to'] ?? '') ? implode(', ', $sectionData['headers']['to']) : ($sectionData['headers']['to'] ?? ''); $parts['reply_to'] = is_array($sectionData['headers']['reply-to'] ?? '') ? implode(', ', $sectionData['headers']['reply-to']) : ($sectionData['headers']['reply-to'] ?? ''); $parts['subject'] = is_array($sectionData['headers']['subject'] ?? '') ? implode(', ', $sectionData['headers']['subject']) : ($sectionData['headers']['subject'] ?? ''); $parts['date'] = is_array($sectionData['headers']['date'] ?? '') ? implode(', ', $sectionData['headers']['date']) : ($sectionData['headers']['date'] ?? ''); } // Handle content $contentType = $sectionData['content-type'] ?? 'text/plain'; $transferEncoding = $sectionData['transfer-encoding'] ?? '7bit'; $charset = $sectionData['charset'] ?? 'UTF-8'; // Extract Content-ID from headers (mailparse may store it in headers, not sectionData) // Handle various case variations: Content-ID, Content-Id, content-id, etc. $contentId = ''; if (isset($sectionData['content-id'])) { $contentId = trim($sectionData['content-id'], '<>'); } elseif (isset($sectionData['content-id-header'])) { $contentId = trim($sectionData['content-id-header'], '<>'); } elseif (isset($sectionData['headers'])) { // Check all possible case variations in headers $headerKeys = ['Content-ID', 'Content-Id', 'content-id', 'content-ID', 'CONTENT-ID']; foreach ($headerKeys as $key) { if (isset($sectionData['headers'][$key])) { $contentId = is_array($sectionData['headers'][$key]) ? trim($sectionData['headers'][$key][0], '<>') : trim($sectionData['headers'][$key], '<>'); break; } } // If still not found, try case-insensitive search through all headers if (empty($contentId)) { foreach ($sectionData['headers'] as $headerKey => $headerValue) { if (stripos($headerKey, 'content-id') !== false || stripos($headerKey, 'contentid') !== false) { $contentId = is_array($headerValue) ? trim($headerValue[0], '<>') : trim($headerValue, '<>'); break; } } } } // Additional fallback: try to extract from raw email section if available if (empty($contentId) && isset($sectionData['starting-pos-body']) && isset($sectionData['starting-pos'])) { $sectionStart = $sectionData['starting-pos'] ?? 0; $sectionBodyStart = $sectionData['starting-pos-body'] ?? $sectionStart; // Extract the header section for this part $headerSection = substr($rawEmail, $sectionStart, $sectionBodyStart - $sectionStart); // Try to match Content-ID header with case-insensitive regex if (preg_match('/Content-ID:\s*([^\r\n]+)/i', $headerSection, $matches)) { $contentId = trim($matches[1], '<>'); } } // Debug logging for Content-ID extraction (only for image types to reduce noise) if (stripos($contentType, 'image/') !== false) { if (!empty($contentId)) { ErrorLog::logInfo('MailMessages::parseRawEmail: Extracted Content-ID for image: "' . $contentId . '" (Content-Type: ' . $contentType . ')'); } else { ErrorLog::logInfo('MailMessages::parseRawEmail: WARNING - No Content-ID found for image attachment (Content-Type: ' . $contentType . ')'); } } // Extract Content-Disposition $contentDisposition = ''; if (isset($sectionData['content-disposition'])) { $contentDisposition = strtolower($sectionData['content-disposition']); } elseif (isset($sectionData['headers']['content-disposition'])) { $contentDisposition = is_array($sectionData['headers']['content-disposition']) ? strtolower($sectionData['headers']['content-disposition'][0]) : strtolower($sectionData['headers']['content-disposition']); } elseif (isset($sectionData['headers']['Content-Disposition'])) { $contentDisposition = is_array($sectionData['headers']['Content-Disposition']) ? strtolower($sectionData['headers']['Content-Disposition'][0]) : strtolower($sectionData['headers']['Content-Disposition']); } // Extract disposition filename $dispositionFilename = ''; if (isset($sectionData['disposition-filename'])) { $dispositionFilename = $sectionData['disposition-filename']; } elseif (!empty($contentDisposition) && preg_match('/filename="?([^";]+)"?/i', $contentDisposition, $matches)) { $dispositionFilename = trim($matches[1]); } // Check if we have valid body positions if (isset($sectionData['starting-pos-body']) && isset($sectionData['ending-pos-body'])) { $body = substr($rawEmail, $sectionData['starting-pos-body'], $sectionData['ending-pos-body'] - $sectionData['starting-pos-body']); // Decode body $decodedBody = self::decodeBody($body, $transferEncoding, $charset, $contentType); // Assign HTML or text if (stripos($contentType, 'text/html') !== false) { $parts['html'] .= $decodedBody; $parts['content_type'] = 'text/html'; } elseif (stripos($contentType, 'text/plain') !== false) { $parts['text'] .= $decodedBody; } else { // Handle attachments and inline images // Determine if this is an attachment or inline image based on Content-Disposition and Content-ID $isAttachment = (stripos($contentDisposition, 'attachment') !== false); $isInline = (stripos($contentDisposition, 'inline') !== false) || (empty($contentDisposition) && !empty($contentId) && stripos($contentType, 'image/') !== false); // Extract filename $filename = ''; if (!empty($dispositionFilename)) { $filename = $dispositionFilename; } elseif (!empty($contentId)) { $filename = $contentId; // Add extension based on content type for inline images if (stripos($contentType, 'image/jpeg') !== false || stripos($contentType, 'image/jpg') !== false) { $filename .= '.jpg'; } elseif (stripos($contentType, 'image/png') !== false) { $filename .= '.png'; } elseif (stripos($contentType, 'image/gif') !== false) { $filename .= '.gif'; } } // Determine if this is an attachment or inline image // Inline images: have Content-ID, are images, and don't have Content-Disposition: attachment // Attachments: have Content-Disposition: attachment OR are application/* without Content-ID if ($isAttachment || (stripos($contentType, 'application/') !== false && empty($contentId)) || (!empty($dispositionFilename) && !$isInline && !empty($contentDisposition))) { // This is an attachment if (empty($filename)) { $filename = 'attachment_' . count($parts['attachments']); } $parts['attachments'][] = [ 'filename' => $filename, 'content' => $decodedBody, 'content_type' => $contentType, 'content_id' => $contentId ]; } elseif ($isInline || (!empty($contentId) && stripos($contentType, 'image/') !== false)) { // This is an inline/embedded image // Store it as an attachment but mark it clearly as inline via content_id // ToSendProcess.php will use content_id to identify it as embedded if (empty($filename)) { $filename = $contentId ?: 'inline_image_' . count($parts['attachments']); } $parts['attachments'][] = [ 'filename' => $filename, 'content' => $decodedBody, 'content_type' => $contentType, 'content_id' => $contentId ]; } } } } mailparse_msg_free($mime); return $parts; } /** * Decode email body based on transfer encoding * @param string $body * @param string $encoding * @param string $charset * @param string $contentType Content type to determine if binary (optional) * @return string */ private static function decodeBody($body, $encoding, $charset, $contentType = '') { switch (strtolower($encoding)) { case 'base64': $decoded = base64_decode($body, true); if ($decoded === false) { $decoded = $body; } break; case 'quoted-printable': $decoded = self::robustQuotedPrintableDecode($body); break; default: $decoded = $body; } // Determine if this is binary content (should not have charset conversion applied) // Binary content: anything that's not text/* or message/* $isBinary = !empty($contentType) && !(stripos($contentType, 'text/') === 0 || stripos($contentType, 'message/') === 0); // Convert charset to UTF-8 if needed - BUT NOT for binary content (PDFs, images, etc.) // Binary content should never have charset conversion applied as it corrupts the data if (!$isBinary && strtoupper($charset) !== 'UTF-8' && function_exists('mb_convert_encoding')) { $decoded = @mb_convert_encoding($decoded, 'UTF-8', $charset); } return $decoded; } /** * Extract boundary from message body * @param string $body * @return string */ private static function extractBoundaryFromBody($body) { // Look for boundary pattern in the body (exact format from your example) if (preg_match('/^--------------([A-Za-z0-9]+)$/m', $body, $matches)) { return $matches[1]; } // Look for boundary in Content-Type header within the body if (preg_match('/Content-Type:\s*multipart\/[^;]+;\s*boundary="?([^";\r\n]+)"?/i', $body, $matches)) { return $matches[1]; } // Look for any line that starts with dashes and contains alphanumeric characters if (preg_match('/^--+([A-Za-z0-9]+)$/m', $body, $matches)) { return $matches[1]; } // Look for boundary in headers (for multipart messages) if (preg_match('/boundary="?([^";\r\n]+)"?/i', $body, $matches)) { return $matches[1]; } return ''; } /** * Extract all boundaries from a complex multipart message * @param string $body * @return array Array of boundaries found */ private static function extractAllBoundaries($body) { $boundaries = []; // Look for all boundary patterns in the body if (preg_match_all('/^--+([A-Za-z0-9_]+)$/m', $body, $matches)) { foreach ($matches[1] as $boundary) { // Validate boundary length and content if (strlen($boundary) > 0 && strlen($boundary) <= 100 && !in_array($boundary, $boundaries)) { $boundaries[] = $boundary; } } } // Also look for boundaries in Content-Type headers if (preg_match_all('/boundary="?([^";\r\n]+)"?/i', $body, $matches)) { foreach ($matches[1] as $boundary) { // Validate boundary length and content if (strlen($boundary) > 0 && strlen($boundary) <= 100 && !in_array($boundary, $boundaries)) { $boundaries[] = $boundary; } } } // Look for Microsoft-specific boundary patterns (like _012_VI0P190MB2389...) if (preg_match_all('/^--_([0-9]+_[A-Z0-9]+_[A-Z0-9]+)$/m', $body, $matches)) { foreach ($matches[1] as $boundary) { // Validate boundary length and content if (strlen($boundary) > 0 && strlen($boundary) <= 100 && !in_array($boundary, $boundaries)) { $boundaries[] = $boundary; } } } // Look for Microsoft Outlook specific boundary patterns if (preg_match_all('/^--_([0-9]+_[A-Z0-9]+_[A-Z0-9]+_[A-Z0-9]+)$/m', $body, $matches)) { foreach ($matches[1] as $boundary) { // Validate boundary length and content if (strlen($boundary) > 0 && strlen($boundary) <= 100 && !in_array($boundary, $boundaries)) { $boundaries[] = $boundary; } } } // Look for boundaries in the body that might be missed by other patterns if (preg_match_all('/--([A-Za-z0-9_]+)/', $body, $matches)) { foreach ($matches[1] as $boundary) { // Skip if it's just dashes if ($boundary !== '' && strlen($boundary) <= 100 && !in_array($boundary, $boundaries)) { $boundaries[] = $boundary; } } } // Limit the total number of boundaries to prevent memory issues $maxBoundaries = 50; if (count($boundaries) > $maxBoundaries) { // ErrorLog::logInfo('Too many boundaries found (' . count($boundaries) . '), limiting to ' . $maxBoundaries . ' to prevent memory issues'); $boundaries = array_slice($boundaries, 0, $maxBoundaries); } return $boundaries; } /** * Parse complex multipart message with nested boundaries * @param string $body * @param array $boundaries Array of all boundaries found * @param array $parts * @param bool $debug Enable debug logging * @param array &$debugMessages Array to collect debug messages * @param int $recursionDepth Current recursion depth (internal use) * @return array */ private static function parseComplexMultipartMessage($body, $boundaries, $parts, $debug = false, &$debugMessages = [], $recursionDepth = 0) { // Limit recursion depth to prevent infinite loops and memory issues if ($recursionDepth > 5) { if ($debug) { $debugMessages[] = "MailMessages: Maximum recursion depth (5) reached, processing as simple message"; } return self::parseSimpleMessage($body, $parts, $debug, $debugMessages); } // Add size limit to prevent memory issues with extremely large emails $maxBodySize = 20 * 1024 * 1024; // 20MB max if (strlen($body) > $maxBodySize) { if ($debug) { $debugMessages[] = "MailMessages: Body too large (" . round(strlen($body) / 1024 / 1024, 2) . "MB), truncating to prevent memory issues"; } $body = substr($body, 0, $maxBodySize); } if ($debug) { $debugMessages[] = "MailMessages: Processing complex multipart with " . count($boundaries) . " boundaries (recursion depth: " . $recursionDepth . ")"; } // Limit the number of boundaries to process to prevent memory issues $maxBoundaries = 50; if (count($boundaries) > $maxBoundaries) { if ($debug) { $debugMessages[] = "MailMessages: Too many boundaries (" . count($boundaries) . "), limiting to " . $maxBoundaries . " to prevent memory issues"; } $boundaries = array_slice($boundaries, 0, $maxBoundaries); } // Sort boundaries by length (longest first) to handle nested boundaries properly usort($boundaries, function($a, $b) { return strlen($b) - strlen($a); }); // Start with the outermost boundary $primaryBoundary = $boundaries[0]; $boundaryMarker = '--' . $primaryBoundary; if ($debug) { $debugMessages[] = "MailMessages: Using primary boundary: " . $primaryBoundary; } // Split by primary boundary $messageParts = explode($boundaryMarker, $body); if ($debug) { $debugMessages[] = "MailMessages: Found " . count($messageParts) . " primary message parts"; } // Limit the number of message parts to process $maxMessageParts = 50; if (count($messageParts) > $maxMessageParts) { if ($debug) { $debugMessages[] = "MailMessages: Too many message parts (" . count($messageParts) . "), limiting to " . $maxMessageParts . " to prevent memory issues"; } $messageParts = array_slice($messageParts, 0, $maxMessageParts); } // Process parts in order to maintain proper content hierarchy foreach ($messageParts as $index => $messagePart) { $messagePart = trim($messagePart); // Skip empty parts, boundary markers, and final boundary if (empty($messagePart) || $messagePart === '--' || strpos($messagePart, '--') === 0) { continue; } if ($debug) { $debugMessages[] = "MailMessages: Processing part " . ($index + 1) . " with length " . strlen($messagePart); } // Check if this part contains nested multipart content $nestedBoundaries = self::extractAllBoundaries($messagePart); if (!empty($nestedBoundaries)) { // Recursively parse nested multipart with increased recursion depth $nestedParts = self::parseComplexMultipartMessage($messagePart, $nestedBoundaries, $parts, $debug, $debugMessages, $recursionDepth + 1); // Merge the nested parts, prioritizing HTML over text if (!empty($nestedParts['html_message'])) { $parts['html_message'] = $nestedParts['html_message']; if ($debug) { $debugMessages[] = "MailMessages: Found HTML content in nested part, length: " . strlen($nestedParts['html_message']); } } if (!empty($nestedParts['text_message']) && empty($parts['text_message'])) { $parts['text_message'] = $nestedParts['text_message']; if ($debug) { $debugMessages[] = "MailMessages: Found text content in nested part, length: " . strlen($nestedParts['text_message']); } } if (!empty($nestedParts['attachments'])) { $parts['attachments'] = array_merge($parts['attachments'], $nestedParts['attachments']); } } else { // Process as regular part $processedParts = self::processMessagePart($messagePart, $parts, $debug, $debugMessages); // Update parts with processed content, prioritizing HTML if (!empty($processedParts['html_message'])) { $parts['html_message'] = $processedParts['html_message']; if ($debug) { $debugMessages[] = "MailMessages: Found HTML content in regular part, length: " . strlen($processedParts['html_message']); } } if (!empty($processedParts['text_message']) && empty($parts['text_message'])) { $parts['text_message'] = $processedParts['text_message']; if ($debug) { $debugMessages[] = "MailMessages: Found text content in regular part, length: " . strlen($processedParts['text_message']); } } if (!empty($processedParts['attachments'])) { $parts['attachments'] = array_merge($parts['attachments'], $processedParts['attachments']); } } } // If we still don't have HTML content, try to extract it from the raw body if (empty($parts['html_message'])) { $htmlContent = self::extractHtmlFromRawBody($body); if (!empty($htmlContent)) { $parts['html_message'] = $htmlContent; if ($debug) { $debugMessages[] = "MailMessages: Extracted HTML content from raw body, length: " . strlen($htmlContent); } } } return $parts; } /** * Parse multipart message (legacy method for backward compatibility) * @param string $body * @param string $boundaryMarker * @param array $parts * @param bool $debug Enable debug logging * @param array &$debugMessages Array to collect debug messages * @return array */ private static function parseMultipartMessage($body, $boundaryMarker, $parts, $debug = false, &$debugMessages = []) { // Split by boundary markers $messageParts = explode($boundaryMarker, $body); if ($debug) { $debugMessages[] = "MailMessages: Found " . count($messageParts) . " message parts"; } foreach ($messageParts as $messagePart) { $messagePart = trim($messagePart); // Skip empty parts, boundary markers, and final boundary if (empty($messagePart) || $messagePart === '--' || strpos($messagePart, '--') === 0) { continue; } // Split part into headers and content $partHeaderEnd = strpos($messagePart, "\r\n\r\n"); if ($partHeaderEnd === false) { $partHeaderEnd = strpos($messagePart, "\n\n"); } if ($partHeaderEnd !== false) { $partHeaders = substr($messagePart, 0, $partHeaderEnd); $partContent = substr($messagePart, $partHeaderEnd + 2); } else { $partHeaders = $messagePart; $partContent = ""; } // Parse part headers $partHeaderLines = explode("\n", $partHeaders); $partHeaderData = []; $currentPartHeader = ""; $currentPartValue = ""; foreach ($partHeaderLines as $line) { $line = trim($line); if (empty($line)) continue; if (preg_match('/^\s/', $line)) { // Continuation line $currentPartValue .= " " . trim($line); } else { // Save previous header if (!empty($currentPartHeader)) { $partHeaderData[$currentPartHeader] = $currentValue; } // Start new header $colonPos = strpos($line, ':'); if ($colonPos !== false) { $currentPartHeader = trim(substr($line, 0, $colonPos)); $currentPartValue = trim(substr($line, $colonPos + 1)); } } } // Save last header if (!empty($currentPartHeader)) { $partHeaderData[$currentPartHeader] = $currentPartValue; } // Process content based on content type // Each MIME part (text/plain and text/html) is decoded separately if (isset($partHeaderData['Content-Type'])) { $contentType = $partHeaderData['Content-Type']; $encoding = isset($partHeaderData['Content-Transfer-Encoding']) ? strtolower(trim($partHeaderData['Content-Transfer-Encoding'])) : ''; if ($debug) { $debugMessages[] = "MailMessages: Processing part with Content-Type: " . $contentType; $debugMessages[] = "MailMessages: Content-Transfer-Encoding: " . $encoding; } // Decode each MIME part separately based on its own Content-Transfer-Encoding // This ensures text/plain and text/html are decoded independently $decodedContent = $partContent; if ($encoding === 'base64') { $decodedContent = base64_decode($partContent); if ($decodedContent === false) { $decodedContent = $partContent; // Fallback to original } } elseif ($encoding === 'quoted-printable') { // Decode this specific part using robust decoder $decodedContent = self::robustQuotedPrintableDecode($partContent); } // Store decoded content separately for text/plain and text/html if (strpos($contentType, 'text/plain') !== false) { // Decode and store text/plain part separately $parts['text_message'] = trim($decodedContent); if ($debug) { $debugMessages[] = "MailMessages: Stored text/plain content, length: " . strlen($decodedContent); $debugMessages[] = "MailMessages: Text content preview: " . substr($decodedContent, 0, 100); } } elseif (strpos($contentType, 'text/html') !== false) { // Decode and store text/html part separately $parts['html_message'] = trim($decodedContent); if ($debug) { $debugMessages[] = "MailMessages: Stored text/html content, length: " . strlen($decodedContent); $debugMessages[] = "MailMessages: HTML content preview: " . substr($decodedContent, 0, 100); } } elseif (strpos($contentType, 'multipart/') !== false) { // For nested multipart, try to parse it recursively $nestedBoundary = self::extractBoundaryFromBody($decodedContent); if (!empty($nestedBoundary)) { $nestedBoundaryMarker = '--' . $nestedBoundary; $nestedParts = self::parseMultipartMessage($decodedContent, $nestedBoundaryMarker, $parts, $debug, $debugMessages); // Merge the nested parts if (!empty($nestedParts['html_message'])) { $parts['html_message'] = $nestedParts['html_message']; } if (!empty($nestedParts['text_message'])) { $parts['text_message'] = $nestedParts['text_message']; } // Merge attachments if (!empty($nestedParts['attachments'])) { $parts['attachments'] = array_merge($parts['attachments'], $nestedParts['attachments']); } } else { $parts['multipart_message'] = trim($decodedContent); } if ($debug) { $debugMessages[] = "MailMessages: Processed nested multipart content, length: " . strlen($decodedContent); } } elseif (strpos($contentType, 'image/') !== false || strpos($contentType, 'application/') !== false) { // Handle binary content (images, documents, etc.) $parts['multipart_message'] .= "\n\n" . $partHeaders . "\n\n" . $partContent; } // Handle attachments if ( isset($partHeaderData['Content-Disposition']) && strpos($partHeaderData['Content-Disposition'], 'attachment') !== false ) { $attachment = [ 'filename' => '', 'content_type' => $contentType, 'size' => strlen($decodedContent), 'content_id' => isset($partHeaderData['Content-ID']) ? trim($partHeaderData['Content-ID'], '<>') : '', 'disposition' => $partHeaderData['Content-Disposition'] ]; // Extract filename if (preg_match('/filename="([^"]+)"/', $partHeaderData['Content-Disposition'], $matches)) { $attachment['filename'] = $matches[1]; } elseif (preg_match('/filename=([^;\r\n]+)/', $partHeaderData['Content-Disposition'], $matches)) { $attachment['filename'] = trim($matches[1]); } $parts['attachments'][] = $attachment; } } } return $parts; } /** * Process a single message part * @param string $messagePart * @param array $parts * @param bool $debug Enable debug logging * @param array &$debugMessages Array to collect debug messages * @return array */ private static function processMessagePart($messagePart, $parts, $debug = false, &$debugMessages = []) { // Split part into headers and content $partHeaderEnd = strpos($messagePart, "\r\n\r\n"); if ($partHeaderEnd === false) { $partHeaderEnd = strpos($messagePart, "\n\n"); } if ($partHeaderEnd !== false) { $partHeaders = substr($messagePart, 0, $partHeaderEnd); $partContent = substr($messagePart, $partHeaderEnd + 2); } else { $partHeaders = $messagePart; $partContent = ""; } // Parse part headers $partHeaderLines = explode("\n", $partHeaders); $partHeaderData = []; $currentPartHeader = ""; $currentPartValue = ""; foreach ($partHeaderLines as $line) { $line = trim($line); if (empty($line)) continue; if (preg_match('/^\s/', $line)) { // Continuation line $currentPartValue .= " " . trim($line); } else { // Save previous header if (!empty($currentPartHeader)) { $partHeaderData[$currentPartHeader] = $currentPartValue; } // Start new header $colonPos = strpos($line, ':'); if ($colonPos !== false) { $currentPartHeader = trim(substr($line, 0, $colonPos)); $currentPartValue = trim(substr($line, $colonPos + 1)); } } } // Save last header if (!empty($currentPartHeader)) { $partHeaderData[$currentPartHeader] = $currentPartValue; } // Process content based on content type if (isset($partHeaderData['Content-Type'])) { $contentType = $partHeaderData['Content-Type']; $encoding = isset($partHeaderData['Content-Transfer-Encoding']) ? strtolower(trim($partHeaderData['Content-Transfer-Encoding'])) : ''; if ($debug) { $debugMessages[] = "MailMessages: Processing part with Content-Type: " . $contentType; $debugMessages[] = "MailMessages: Content-Transfer-Encoding: " . $encoding; } // Decode each MIME part separately based on its own Content-Transfer-Encoding // This ensures text/plain and text/html are decoded independently $decodedContent = $partContent; if ($encoding === 'base64') { $decodedContent = base64_decode($partContent); if ($decodedContent === false) { $decodedContent = $partContent; // Fallback to original } } elseif ($encoding === 'quoted-printable') { // Decode this specific part using robust decoder $decodedContent = self::robustQuotedPrintableDecode($partContent); } // Store decoded content separately for text/plain and text/html if (strpos($contentType, 'text/plain') !== false) { // Decode and store text/plain part separately // Only store text if we don't already have HTML content if (empty($parts['html_message'])) { $parts['text_message'] = trim($decodedContent); if ($debug) { $debugMessages[] = "MailMessages: Stored text/plain content, length: " . strlen($decodedContent); $debugMessages[] = "MailMessages: Text content preview: " . substr($decodedContent, 0, 100); } } } elseif (strpos($contentType, 'text/html') !== false) { // Decode and store text/html part separately // Always prioritize HTML content over text $parts['html_message'] = trim($decodedContent); if ($debug) { $debugMessages[] = "MailMessages: Stored text/html content, length: " . strlen($decodedContent); $debugMessages[] = "MailMessages: HTML content preview: " . substr($decodedContent, 0, 100); } } elseif (strpos($contentType, 'multipart/') !== false) { // For nested multipart, try to parse it recursively $nestedBoundary = self::extractBoundaryFromBody($decodedContent); if (!empty($nestedBoundary)) { $nestedBoundaryMarker = '--' . $nestedBoundary; $nestedParts = self::parseMultipartMessage($decodedContent, $nestedBoundaryMarker, $parts, $debug, $debugMessages); // Merge the nested parts, prioritizing HTML if (!empty($nestedParts['html_message'])) { $parts['html_message'] = $nestedParts['html_message']; } if (!empty($nestedParts['text_message']) && empty($parts['text_message'])) { $parts['text_message'] = $nestedParts['text_message']; } // Merge attachments if (!empty($nestedParts['attachments'])) { $parts['attachments'] = array_merge($parts['attachments'], $nestedParts['attachments']); } } else { $parts['multipart_message'] = trim($decodedContent); } if ($debug) { $debugMessages[] = "MailMessages: Processed nested multipart content, length: " . strlen($decodedContent); } } elseif (strpos($contentType, 'image/') !== false || strpos($contentType, 'application/') !== false) { // Handle binary content (images, documents, etc.) $parts['multipart_message'] .= "\n\n" . $partHeaders . "\n\n" . $partContent; } // Handle attachments if ( isset($partHeaderData['Content-Disposition']) && strpos($partHeaderData['Content-Disposition'], 'attachment') !== false ) { $attachment = [ 'filename' => '', 'content_type' => $contentType, 'size' => strlen($decodedContent), 'content_id' => isset($partHeaderData['Content-ID']) ? trim($partHeaderData['Content-ID'], '<>') : '', 'disposition' => $partHeaderData['Content-Disposition'] ]; // Extract filename if (preg_match('/filename="([^"]+)"/', $partHeaderData['Content-Disposition'], $matches)) { $attachment['filename'] = $matches[1]; } elseif (preg_match('/filename=([^;\r\n]+)/', $partHeaderData['Content-Disposition'], $matches)) { $attachment['filename'] = trim($matches[1]); } $parts['attachments'][] = $attachment; } } return $parts; } /** * Custom quoted-printable decoder for problematic Microsoft content * Decodes each MIME part separately (text/plain and text/html) * @param string $text The quoted-printable encoded text * @return string The decoded text */ private static function robustQuotedPrintableDecode($text) { // Normalize line endings to LF for consistent processing $text = str_replace(["\r\n", "\r"], "\n", $text); // Remove soft line breaks (lines ending with '=' followed by newline) // This must be done BEFORE calling quoted_printable_decode() // Pattern: "=" at end of line followed by newline $hadSoftBreaks = (bool) preg_match("/=\s*\n/", $text); $text = preg_replace("/=\n/", '', $text); // Also handle cases with optional whitespace: "= \n" or "=\t\n" $text = preg_replace("/=\s*\n/", '', $text); // Only decode when quoted-printable markers are present. // Plain URLs like "?id=511" are valid text and must not be decoded. $hasEncodedHex = (bool) preg_match('/=[0-9A-F]{2}/i', $text); if ($hadSoftBreaks || $hasEncodedHex) { // Decode quoted-printable characters using PHP's built-in function $decoded = quoted_printable_decode($text); // Handle any remaining =XX sequences that might have been missed $decoded = preg_replace_callback('/=([0-9A-F]{2})/i', function($matches) { return chr(hexdec($matches[1])); }, $decoded); } else { $decoded = $text; } // Remove soft line break artifacts: "=" at end of line (likely soft break artifact) // Pattern: word character + "=" at end of line // This is safe because legitimate "=" in quoted-printable is encoded as "=3D" $decoded = preg_replace('/([a-zA-Z0-9])=\s*$/m', '$1', $decoded); return $decoded; } /** * Extract the actual email content from complex multipart structures * This method specifically handles the email format you showed * @param string $rawEmail * @return array */ private static function extractEmailContent($rawEmail) { $content = [ 'text' => '', 'html' => '', 'subject' => '', 'from' => '', 'to' => '' ]; // Look for the actual email content after all the headers $lines = explode("\n", $rawEmail); $inContent = false; $contentLines = []; foreach ($lines as $line) { // Skip until we find the actual email content if (strpos($line, 'Content-Type: text/') !== false) { $inContent = true; continue; } if ($inContent && !empty(trim($line))) { $contentLines[] = $line; } } if (!empty($contentLines)) { $content['text'] = implode("\n", $contentLines); } return $content; } /** * Handle emails with "-----Original Message-----" structure * This is common in forwarded emails from Microsoft Outlook * @param string $rawEmail * @return array */ private static function handleOriginalMessageFormat($rawEmail) { $content = [ 'text' => '', 'html' => '', 'subject' => '', 'from' => '', 'to' => '' ]; // Look for the "-----Original Message-----" marker if (strpos($rawEmail, '-----Original Message-----') !== false) { $parts = explode('-----Original Message-----', $rawEmail); // The first part should contain the actual email content if (!empty($parts[0])) { $firstPart = trim($parts[0]); // Extract subject, from, to from the first part if (preg_match('/Subject:\s*(.+)$/m', $firstPart, $matches)) { $content['subject'] = trim($matches[1]); } if (preg_match('/From:\s*(.+)$/m', $firstPart, $matches)) { $content['from'] = trim($matches[1]); } if (preg_match('/To:\s*(.+)$/m', $firstPart, $matches)) { $content['to'] = trim($matches[1]); } // Look for the actual message content $lines = explode("\n", $firstPart); $messageLines = []; $inMessage = false; foreach ($lines as $line) { $line = trim($line); // Skip headers if (preg_match('/^(From|To|Subject|Sent|Date):/i', $line)) { continue; } // Start collecting message content after we've seen headers if (!empty($line) && !$inMessage) { $inMessage = true; } if ($inMessage) { $messageLines[] = $line; } } if (!empty($messageLines)) { $content['text'] = implode("\n", $messageLines); } } } return $content; } /** * Parse simple (non-multipart) message * @param string $body * @param array $parts * @param bool $debug Enable debug logging * @param array &$debugMessages Array to collect debug messages * @return array */ private static function parseSimpleMessage($body, $parts, $debug = false, &$debugMessages = []) { $contentType = isset($parts['headers']['Content-Type']) ? $parts['headers']['Content-Type'] : ''; $encoding = isset($parts['headers']['Content-Transfer-Encoding']) ? strtolower(trim($parts['headers']['Content-Transfer-Encoding'])) : ''; // Decode content if needed $decodedBody = $body; if ($encoding === 'base64') { $decodedBody = base64_decode($body); if ($decodedBody === false) { $decodedBody = $body; // Fallback to original } } elseif ($encoding === 'quoted-printable') { $decodedBody = self::robustQuotedPrintableDecode($body); } // Determine content type if (strpos($contentType, 'text/html') !== false) { $parts['html_message'] = trim($decodedBody); if ($debug) { $debugMessages[] = "MailMessages: Simple message - stored as HTML, length: " . strlen($decodedBody); } } else { // Check if content looks like HTML $trimmedBody = trim($decodedBody); if ( strpos($trimmedBody, '<!DOCTYPE html>') === 0 || strpos($trimmedBody, '<html') === 0 || (strpos($trimmedBody, '<') === 0 && strpos($trimmedBody, '</') !== false) ) { $parts['html_message'] = trim($decodedBody); if ($debug) { $debugMessages[] = "MailMessages: Simple message - detected HTML content, length: " . strlen($decodedBody); } } else { $parts['text_message'] = trim($decodedBody); if ($debug) { $debugMessages[] = "MailMessages: Simple message - stored as text, length: " . strlen($decodedBody); } } } return $parts; } /** * Handle truncated or incomplete messages * @param string $body * @param array $parts * @param bool $debug Enable debug logging * @param array &$debugMessages Array to collect debug messages * @return array */ private static function handleTruncatedMessage($body, $parts, $debug = false, &$debugMessages = []) { // If we have no text or HTML content, try to extract what we can if (empty($parts['text_message']) && empty($parts['html_message'])) { $trimmedBody = trim($body); if ($debug) { $debugMessages[] = "MailMessages: No content found, attempting truncated message handling"; $debugMessages[] = "MailMessages: Trimmed body length: " . strlen($trimmedBody); } // Look for any readable content if (!empty($trimmedBody)) { // Check if it looks like HTML if (strpos($trimmedBody, '<') === 0 && strpos($trimmedBody, '>') !== false) { $parts['html_message'] = $trimmedBody; if ($debug) { $debugMessages[] = "MailMessages: Truncated message - stored as HTML, length: " . strlen($trimmedBody); } } else { $parts['text_message'] = $trimmedBody; if ($debug) { $debugMessages[] = "MailMessages: Truncated message - stored as text, length: " . strlen($trimmedBody); } } } } return $parts; } /** * Clean and encode message parts * @param array $parts * @return array */ private static function cleanMessageParts($parts) { // Clean and encode text message if (!empty($parts['text_message'])) { $parts['text_message'] = stripslashes($parts['text_message']); $parts['text_message'] = self::cleanUnicodeCharacters($parts['text_message']); $parts['text_message'] = mb_convert_encoding($parts['text_message'], 'UTF-8', 'AUTO'); } // Clean and encode HTML message if (!empty($parts['html_message'])) { $parts['html_message'] = stripslashes($parts['html_message']); $parts['html_message'] = self::cleanUnicodeCharacters($parts['html_message']); $parts['html_message'] = mb_convert_encoding($parts['html_message'], 'UTF-8', 'AUTO'); } // Clean and encode multipart message if (!empty($parts['multipart_message'])) { $parts['multipart_message'] = stripslashes($parts['multipart_message']); $parts['multipart_message'] = self::cleanUnicodeCharacters($parts['multipart_message']); $parts['multipart_message'] = mb_convert_encoding($parts['multipart_message'], 'UTF-8', 'AUTO'); } // Clean subject if (!empty($parts['subject'])) { $parts['subject'] = stripslashes($parts['subject']); $parts['subject'] = self::cleanUnicodeCharacters($parts['subject']); $parts['subject'] = mb_convert_encoding($parts['subject'], 'UTF-8', 'AUTO'); } return $parts; } /** * Strip MIME headers from message content * @param string $content * @return string */ private static function stripMimeHeaders($content) { // Look for the first double newline which separates headers from body $headerEnd = strpos($content, "\r\n\r\n"); if ($headerEnd === false) { $headerEnd = strpos($content, "\n\n"); } if ($headerEnd !== false) { return substr($content, $headerEnd + 2); } return $content; } /** * Clean Unicode characters that might cause database encoding issues * @param string $text * @return string */ public static function cleanUnicodeCharacters($text) { // Handle null or empty text if (empty($text)) { return ''; } // Convert to UTF-8 if not already if (!mb_check_encoding($text, 'UTF-8')) { $text = mb_convert_encoding($text, 'UTF-8', 'AUTO'); } // Replace problematic Unicode characters with HTML entities or safe alternatives $replacements = [ // Arrows "\xE2\x86\x93" => '↓', // Down arrow "\xE2\x86\x91" => '↑', // Up arrow "\xE2\x86\x90" => '←', // Left arrow "\xE2\x86\x92" => '→', // Right arrow "\xE2\x86\x96" => '↖', // North west arrow "\xE2\x86\x97" => '↗', // North east arrow "\xE2\x86\x98" => '↙', // South west arrow "\xE2\x86\x99" => '↘', // South east arrow // Other common problematic characters "\xE2\x80\x93" => '–', // En dash "\xE2\x80\x94" => '—', // Em dash "\xE2\x80\x98" => "'", // Left single quotation mark "\xE2\x80\x99" => "'", // Right single quotation mark "\xE2\x80\x9C" => '"', // Left double quotation mark "\xE2\x80\x9D" => '"', // Right double quotation mark "\xE2\x80\xA6" => '…', // Horizontal ellipsis "\xC2\xA0" => ' ', // Non-breaking space // Additional problematic characters "\xE2\x80\xA2" => '•', // Bullet "\xE2\x80\xA3" => '‣', // Triangular bullet "\xE2\x80\xB2" => '″', // Double prime "\xE2\x80\xB3" => '‴', // Triple prime "\xE2\x80\xB9" => '‹', // Single left-pointing angle quotation mark "\xE2\x80\xBA" => '›', // Single right-pointing angle quotation mark "\xE2\x80\xBC" => '※', // Reference mark "\xE2\x80\xBD" => '‽', // Interrobang "\xE2\x81\x84" => '⁄', // Fraction slash "\xE2\x81\x85" => '⁅', // Left square bracket with quill "\xE2\x81\x86" => '⁆', // Right square bracket with quill ]; $text = str_replace(array_keys($replacements), array_values($replacements), $text); // Remove control characters (0x00-0x1F, 0x7F-0x9F) except newlines and tabs $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/', '', $text); // Remove any remaining invalid UTF-8 sequences $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/', '', $text); // Remove any characters that are not valid in UTF-8 $text = preg_replace('/[\x{FFFE}\x{FFFF}]/u', '', $text); // Ensure the text is valid UTF-8 $text = mb_convert_encoding($text, 'UTF-8', 'UTF-8'); // Final validation - if conversion fails, return empty string if (!mb_check_encoding($text, 'UTF-8')) { return ''; } return $text; } /** * Ensure database connection uses UTF-8 encoding */ private function ensureUtf8Connection() { global $sqlfMail; // Set character encoding to UTF-8 for the database connection if (method_exists($sqlfMail, 'Execute')) { $sqlfMail->Execute("SET NAMES utf8mb4"); $sqlfMail->Execute("SET CHARACTER SET utf8mb4"); $sqlfMail->Execute("SET character_set_connection=utf8mb4"); } } /** * Check if the email is valid * @param string $email * @return bool */ public function isEmail($email) { return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; } /** * Process the mail message and send it to the recipients * @param string $data * @return void */ public function processSend($data) { global $sqlfMail; // Ensure database connection uses UTF-8 encoding $this->ensureUtf8Connection(); $data = json_decode(base64_decode($data), true); $recipientSQL = $data['recipientSQL']; $mailContents = $data['mailContents']; if (strlen($recipientSQL) <= 0) { // ErrorLog::logInfo('Empty recipientSQL for mail message'); return; } // var_dump($data); // exit; $recipients = $this->sqlf->getAll($recipientSQL); $parts = MailMessages::splitMessageParts($mailContents, true); $origfrom = $parts['from']; $isSceBlessingsFrom = (strpos($parts['from'], "blessings@cesa.co.za") !== false); if ($isSceBlessingsFrom) { $parts['from'] = "sce@cesa.co.za"; } $fromName = ''; $fromMail = ''; if (strpos($parts['from'], '<') !== false) { $fromArr = explode('<', $parts['from']); $fromArr[1] = str_replace('>', '', $fromArr[1]); $fromName = trim($fromArr[0]); $fromMail = trim($fromArr[1]); } else { $fromMail = $parts['from']; } // SCE: replies go to Melissa, not Blessings / generic sce@ mailbox. $replyToMail = $isSceBlessingsFrom ? 'melissa@cesa.co.za' : $fromMail; $isHTML = false; $contentType = ''; // Prioritize HTML content over text content if (strlen($parts['html_message']) > 0) { $parts['message'] = stripslashes($parts['html_message']); $isHTML = true; $contentType = 'text/html'; } elseif (strlen($parts['text_message']) > 0) { $parts['message'] = stripslashes($parts['text_message']); $isHTML = false; $contentType = 'text/plain'; } elseif (strlen($parts['multipart_message']) > 0) { // Only use multipart_message if no HTML or text content was extracted // Strip MIME headers from multipart content $parts['message'] = stripslashes(self::stripMimeHeaders($parts['multipart_message'])); // Check if the multipart content is actually HTML if (stripos($parts['message'], '<html') !== false || stripos($parts['message'], '<body') !== false || stripos($parts['message'], '<div') !== false || stripos($parts['message'], '<p') !== false) { $isHTML = true; $contentType = 'text/html'; } else { $isHTML = false; $contentType = 'text/plain'; } } else { // Fallback to empty message $parts['message'] = ''; } // Set headers based on the actual content type being used $newheaders = "From: " . $parts['from'] . "\r\n" . "Reply-To: " . $replyToMail . "\r\n" . "Content-Type: " . $contentType . "\r\n"; // Final safety check: if content looks like HTML but content type is not HTML, fix it if (!$isHTML && (stripos($parts['message'], '<html') !== false || stripos($parts['message'], '<body') !== false || stripos($parts['message'], '<div') !== false || stripos($parts['message'], '<p') !== false)) { $isHTML = true; $contentType = 'text/html'; $newheaders = "From: " . $parts['from'] . "\r\n" . "Reply-To: " . $replyToMail . "\r\n" . "Content-Type: " . $contentType . "\r\n"; } // Debug logging for message selection // ErrorLog::logInfo('MailMessages: Selected message type - HTML: ' . ($isHTML ? 'true' : 'false')); // ErrorLog::logInfo('MailMessages: Selected message content type: ' . $contentType); // ErrorLog::logInfo('MailMessages: Selected message length: ' . strlen($parts['message'])); // ErrorLog::logInfo('MailMessages: Selected message preview: ' . substr($parts['message'], 0, 200)); $options = [ 'reply-to-address' => $replyToMail, 'from-name' => $fromName, 'isHTML' => $isHTML, 'attachments' => [], 'content-type' => $contentType, 'charset' => 'UTF-8', ]; // . "Disposition-Notification-To: " . $parts['from'] . "\r\n"; $subject = isset($parts['headers']["Subject"]) ? $parts['headers']["Subject"] : ''; // Clean and decode the subject properly $parts['subject'] = html_entity_decode($subject, ENT_QUOTES, 'UTF-8'); // Remove any invalid characters $parts['subject'] = preg_replace('/[^\x{0009}\x{000A}\x{000D}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]/u', '', $parts['subject']); if (strlen(trim($parts['subject'])) <= 0) { // ErrorLog::logInfo('Empty subject for mail message: ' . print_r($data, true)); echo 'Empty subject for mail message: ' . print_r($data, true); return; } if (strlen(trim($parts['message'])) <= 0) { // ErrorLog::logInfo('Empty message for mail message: ' . print_r($data, true)); echo 'Empty message for mail message: ' . print_r($data, true); return; } // echo 'After empty subject and message check: ' . print_r($data, true); // exit; $icount = 0; $j = 0; $batch = 0; $alreadySent = []; $arrSQL = []; $recipients = $this->sqlf->getAll($recipientSQL); foreach ($recipients as $recipientArr) { $recipient = ""; if (!empty($recipientArr["Email"]) && $this->isEmail($recipientArr["Email"])) { if (!in_array($recipientArr["Email"], $alreadySent)) { $recipient = $recipientArr["Email"]; $alreadySent[] = $recipientArr["Email"]; } } if (!empty($recipientArr["Email2"]) && $this->isEmail($recipientArr["Email2"])) { if (!in_array($recipientArr["Email2"], $alreadySent)) { if (!empty($recipient)) { $recipient .= ","; } $recipient .= $recipientArr["Email2"]; $alreadySent[] = $recipientArr["Email2"]; } } $subject = isset($parts['headers']["Subject"]) ? $parts['headers']["Subject"] : ''; if (strtolower($subject) == "test" || $parts['from'] == 'otto@igotafrica.com') $recipient = $parts['from']; if (!empty($recipient)) { $arrSQL[] = "('" . addslashes($recipient) . "', " . "'" . addslashes($recipientArr["OfficeName"]) . "', " . "'" . addslashes($recipientArr["ContactDesignation"]) . "', " . "'" . addslashes($recipientArr["ContactInitials"]) . "', " . "'" . addslashes($recipientArr["ContactSurname"]) . "', " . "'" . addslashes((int) $recipientArr["FirmID"]) . "', " . "*MESSAGEID*)"; $icount++; } if ((strtolower($subject) == "test") && ($icount >= 3)) { break; } } //foreach //mail("julia@xe.co.za", "Bulk email status", $icount . " email messages sent by ".$origfrom, "From: webmaster@cesa.co.za"); //$from = "julia@xe.co.za"; maill( $origfrom, "Bulk email status", "Your bulk email message has been received. " . $icount . " email messages will be sent.", null, array('from-address' => 'info@cesa.co.za', 'from-name' => 'CESA Mail System', 'isHTML' => false) ); $messageId = $sqlfMail->getNewId('Messages', 'MessageID'); $to = isset($parts['headers']['To']) ? $parts['headers']['To'] : ''; // Clean the message content to handle Unicode characters properly $cleanedMessage = self::cleanUnicodeCharacters($parts['message']); $cleanedSubject = self::cleanUnicodeCharacters($parts['subject']); // Remove any existing backslashes that might have been added by addslashes() $cleanedMessage = stripslashes($cleanedMessage); // Additional validation for cleaned content if (empty($cleanedMessage)) { // ErrorLog::logInfo('Message content is empty after cleaning: ' . print_r($parts, true)); $cleanedMessage = 'Message content could not be processed due to encoding issues.'; } if (empty($cleanedSubject)) { // ErrorLog::logInfo('Subject is empty after cleaning: ' . print_r($parts, true)); $cleanedSubject = 'No Subject'; } // Ensure the message content is properly cleaned and doesn't have any slashes $finalMessage = stripslashes($cleanedMessage); // Debug logging // ErrorLog::logInfo('MailMessages: Final message preview: ' . substr($finalMessage, 0, 200)); // ErrorLog::logInfo('MailMessages: Content type: ' . $contentType . ', isHTML: ' . ($isHTML ? 'true' : 'false')); $message = [ 'MessageID' => $messageId, 'SentToGroup' => $to, 'DateSent' => date('Y-m-d H:i:s'), 'MessageFrom' => $parts['from'], 'MessageSize' => strlen($finalMessage), 'Subject' => $cleanedSubject, 'Message' => $finalMessage, 'Options' => json_encode($options, JSON_UNESCAPED_SLASHES), 'Headers' => $newheaders, 'NumRecipients' => $icount, 'SendComplete' => 0, ]; // Insert the message with error handling try { $sqlfMail->autoExecute('Messages', $message, 'INSERT', null, 'MessageID'); } catch (Exception $e) { // ErrorLog::logInfo('Failed to insert message into Messages table: ' . $e->getMessage()); // ErrorLog::logInfo('Message data: ' . print_r($message, true)); // Try with additional cleaning as fallback $message['Message'] = self::cleanUnicodeCharacters($message['Message']); $message['Subject'] = self::cleanUnicodeCharacters($message['Subject']); try { $sqlfMail->autoExecute('Messages', $message, 'INSERT', null, 'MessageID'); } catch (Exception $e2) { // ErrorLog::logInfo('Second attempt to insert message failed: ' . $e2->getMessage()); // Final fallback - use basic ASCII content $message['Message'] = 'Message content could not be processed.'; $message['Subject'] = 'Message Processing Error'; $sqlfMail->autoExecute('Messages', $message, 'INSERT', null, 'MessageID'); } } $sqlfMail->execute("DELETE FROM TempFrom"); $iNumInBatch = 50; for ($i = 0; $i < (count($arrSQL) + $iNumInBatch); $i = $i + $iNumInBatch) { $arrTempSQL = []; for ($j = 0; $j < $iNumInBatch; $j++) { if (isset($arrSQL[$i + $j])) $arrTempSQL[] = $arrSQL[$i + $j]; } if (count($arrTempSQL) > 0) { $sql = "REPLACE INTO TempFrom " . "(Email, OfficeName, ContactDesignation, ContactInitials, ContactSurname, FirmID, MessageID) " . "VALUES " . str_replace("*MESSAGEID*", $messageId, implode(",", $arrTempSQL)); // echo 'Inserting into TempFrom: ' . $sql . "\r\n"; $sqlfMail->execute($sql); } unset($arrTempSQL); } $sql = "INSERT INTO ToSend (SendTo, MessageID, Message) " . "SELECT Email, Messages.MessageID, " . "REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(Messages.Message, '*FirmID*', TempFrom.FirmID), " . "'*Email*', TempFrom.Email), '*OfficeName*', TempFrom.OfficeName), '*ContactDesignation*', " . "TempFrom.ContactDesignation), '*ContactInitials*', TempFrom.ContactInitials), " . "'*ContactSurname*', TempFrom.ContactSurname) " . "FROM Messages, TempFrom " . "WHERE Messages.MessageID=" . $messageId . " AND Messages.MessageID=TempFrom.MessageID"; echo 'Inserting into ToSend: ' . $sql . "\r\n"; $sqlfMail->execute($sql); // var_dump($parts); // exit; // foreach ($recipients as $recipient) { // $this->sendMail($recipient, $mailContents); // } } /** * Process mail message from mailbox configs and send to recipients * * This function: * - Reads emails from configured mailboxes * - Processes and stores them in the Messages table * - Adds entries to ToSend table for cesa-test@igotafrica.com * - Deletes processed emails from mailboxes to prevent reprocessing */ public function processMailFromMailBoxConfigs() { global $sqlfMail; // Check if IMAP extension is available if (!extension_loaded('imap')) { // ErrorLog::logInfo('IMAP extension is not available. Cannot process mailboxes.'); return; } // Ensure database connection uses UTF-8 encoding $this->ensureUtf8Connection(); $mailBoxConfigs = self::getMailBoxConfigs(); // ErrorLog::logInfo('Processing ' . count($mailBoxConfigs) . ' mail boxes'); foreach ($mailBoxConfigs as $configName => $mailBoxConfig) { try { // ErrorLog::logInfo('Processing mailbox: ' . $configName); // Connect to mailbox $mailbox = $this->connectToMailbox($mailBoxConfig); if (!$mailbox) { // ErrorLog::logInfo('Failed to connect to mailbox: ' . $configName); continue; } // Get email count $emailCount = imap_num_msg($mailbox); // ErrorLog::logInfo('Found ' . $emailCount . ' emails in mailbox: ' . $configName); if ($emailCount == 0) { imap_close($mailbox); continue; } // Limit the number of emails processed per mailbox to prevent memory issues $maxEmailsPerMailbox = 100; if ($emailCount > $maxEmailsPerMailbox) { // ErrorLog::logInfo('Limiting processing to ' . $maxEmailsPerMailbox . ' emails out of ' . $emailCount . ' in mailbox: ' . $configName); $emailCount = $maxEmailsPerMailbox; } // Process each email $processedCount = 0; $skippedCount = 0; $errorCount = 0; for ($i = 1; $i <= $emailCount; $i++) { try { // Check if email should be processed (not already processed) if ($this->shouldProcessEmail($mailbox, $i, $configName)) { $this->processEmailFromMailbox($mailbox, $i, $configName, $mailBoxConfig); $processedCount++; // Log progress every 10 emails if ($processedCount % 10 == 0) { // ErrorLog::logInfo('Processed ' . $processedCount . ' emails from mailbox ' . $configName); } } else { // Delete mail if not permitted $this->deleteProcessedEmail($mailbox, $i, $configName); // ErrorLog::logInfo('Skipping email ' . $i . ' from mailbox ' . $configName . ' - already processed or invalid'); $skippedCount++; } } catch (Exception $e) { // ErrorLog::logInfo('Error processing email ' . $i . ' from mailbox ' . $configName . ': ' . $e->getMessage()); $errorCount++; // If we get too many errors, stop processing this mailbox if ($errorCount > 10) { // ErrorLog::logInfo('Too many errors in mailbox ' . $configName . ', stopping processing'); break; } continue; } } // ErrorLog::logInfo('Mailbox ' . $configName . ' processing summary: ' . $processedCount . ' processed, ' . $skippedCount . ' skipped, ' . $errorCount . ' errors'); // Close mailbox connection and permanently delete marked emails imap_close($mailbox, CL_EXPUNGE); // ErrorLog::logInfo('Completed processing mailbox: ' . $configName . ' - deleted processed emails'); // Force garbage collection to free memory if (function_exists('gc_collect_cycles')) { gc_collect_cycles(); } } catch (Exception $e) { // ErrorLog::logInfo('Error processing mailbox ' . $configName . ': ' . $e->getMessage()); continue; } } } /** * Connect to a mailbox using IMAP * @param array $config Mailbox configuration * @return resource|IMAP\Connection|false IMAP connection resource/object or false on failure */ private function connectToMailbox($config) { $host = $config['host']; $port = $config['port']; $username = $config['username']; $password = $config['password']; // Determine connection type based on port // Note: /novalidate-cert bypasses SSL certificate validation to ignore SSL errors $ssl = ''; if ($port == 993) { $ssl = '/ssl/novalidate-cert'; } elseif ($port == 995) { $ssl = '/ssl/novalidate-cert'; } elseif ($port == 143) { $ssl = '/notls'; } else { $ssl = '/notls'; } // Build connection string $connectionString = '{' . $host . ':' . $port . '/imap' . $ssl . '}INBOX'; // ErrorLog::logInfo('Connecting to: ' . $connectionString); // Attempt connection with SSL certificate validation bypass $mailbox = @imap_open($connectionString, $username, $password); if (!$mailbox) { $error = imap_last_error(); // ErrorLog::logInfo('IMAP connection failed: ' . $error . ' (SSL certificate validation bypassed)'); return false; } return $mailbox; } /** * Process a single email from the mailbox * Simple process: Read email, extract basic info, store in Messages table, queue recipients in ToSend table * @param resource|IMAP\Connection $mailbox IMAP connection * @param int $emailNumber Email number * @param string $configName Configuration name * @param array $mailBoxConfig Mailbox configuration */ private function processEmailFromMailbox($mailbox, $emailNumber, $configName, $mailBoxConfig) { global $sqlfMail; // Get email headers using IMAP $headers = imap_headerinfo($mailbox, $emailNumber); if (!$headers) { return; } // Get raw email content $rawEmail = imap_fetchheader($mailbox, $emailNumber) . "\r\n\r\n" . imap_body($mailbox, $emailNumber); if (!$rawEmail) { return; } // Check email size to prevent memory issues $emailSize = strlen($rawEmail); $maxEmailSize = 25 * 1024 * 1024; // 25MB max if ($emailSize > $maxEmailSize) { return; } // Extract basic information from headers $from = isset($headers->from[0]) ? $headers->from[0]->mailbox . '@' . $headers->from[0]->host : ''; if (isset($headers->from[0]->personal)) { $from = $headers->from[0]->personal . ' <' . $from . '>'; } $subject = isset($headers->subject) ? $headers->subject : ''; $date = isset($headers->date) ? $headers->date : ''; $to = isset($headers->to) ? $this->formatEmailAddresses($headers->to) : ''; // Clean subject if (empty(trim($subject))) { $subject = 'No Subject'; } $cleanedSubject = self::cleanUnicodeCharacters($subject); // Preserve the complete raw email structure exactly as received // This ensures ToSendProcess.php can properly parse MIME structure, // identify inline images vs attachments, and extract HTML content correctly // We only process quoted-printable soft line breaks in text/html and text/plain parts $processedMessage = $this->preserveCompleteEmailStructure($rawEmail, []); // Extract headers section for storage $storedHeaders = $this->extractRawHeaders($rawEmail); // Use parseRawEmail to properly detect HTML and extract content type // This ensures we correctly identify the message type $isHTML = false; $contentType = 'text/plain'; $parsed = null; if (extension_loaded('mailparse')) { try { $parsed = self::parseRawEmail($rawEmail); if (!empty($parsed['html'])) { $isHTML = true; $contentType = 'text/html'; } elseif (!empty($parsed['text'])) { $isHTML = false; $contentType = 'text/plain'; } } catch (Exception $e) { // Fallback to simple detection if parsing fails $isHTML = (stripos($processedMessage, '<html') !== false || stripos($processedMessage, '<body') !== false || stripos($processedMessage, '<div') !== false || stripos($processedMessage, '<p') !== false); } } else { // Fallback to simple detection if mailparse not available $isHTML = (stripos($processedMessage, '<html') !== false || stripos($processedMessage, '<body') !== false || stripos($processedMessage, '<div') !== false || stripos($processedMessage, '<p') !== false); } // Generate message ID $messageId = $sqlfMail->getNewId('Messages', 'MessageID'); // Extract from-name and reply-to from headers // Always extract the sender's name from the email headers $fromName = ''; if (isset($headers->from[0]->personal) && !empty($headers->from[0]->personal)) { // Use the personal name from the From header $fromName = trim($headers->from[0]->personal); } else { // Fallback: try to extract from the formatted $from string $fromName = $this->extractNameFromEmail($from); } // Strip single and double quotes from from-name $fromName = trim($fromName, '\'"'); // Extract sender email address $senderEmail = isset($headers->from[0]) ? $headers->from[0]->mailbox . '@' . $headers->from[0]->host : ''; // Set reply-to address: use sender's email if it's @cesa.co.za, otherwise use reply-to header. // BCE bulk mailbox always replies to Blessings; SCE mail from blessings@ replies to Melissa. $replyTo = ''; if ($configName === 'bce') { $replyTo = 'blessings@cesa.co.za'; } elseif (!empty($senderEmail) && strcasecmp($senderEmail, 'blessings@cesa.co.za') === 0) { $replyTo = 'melissa@cesa.co.za'; } elseif (!empty($senderEmail) && strpos($senderEmail, '@cesa.co.za') !== false) { // Sender is from @cesa.co.za, use their email as reply-to $replyTo = $senderEmail; } else { // Use reply-to header if available $replyTo = isset($headers->reply_to[0]) ? $headers->reply_to[0]->mailbox . '@' . $headers->reply_to[0]->host : ''; } // Prepare message data // IMPORTANT: Always use the sender's name for from-name (never use default) $message = [ 'MessageID' => $messageId, 'SentToGroup' => $to, 'DateSent' => $this->parseEmailDate($date), 'MessageFrom' => $from, 'MessageSize' => strlen($processedMessage), 'Subject' => $cleanedSubject, 'Message' => $processedMessage, 'Options' => json_encode([ 'reply-to-address' => $replyTo, 'from-name' => $fromName, // Always use sender's name (can be empty, but no default) 'isHTML' => $isHTML, 'content-type' => $contentType, 'charset' => 'UTF-8', 'source' => 'mailbox-' . $configName ], JSON_UNESCAPED_SLASHES), 'Headers' => $storedHeaders, 'NumRecipients' => 0, // Will be set when adding to ToSend 'SendComplete' => 0, ]; // Insert message into Messages table try { $sqlfMail->autoExecute('Messages', $message, 'INSERT', null, 'MessageID'); } catch (Exception $e) { // Try with additional cleaning $message['Message'] = self::cleanUnicodeCharacters($message['Message']); $message['Subject'] = self::cleanUnicodeCharacters($message['Subject']); try { $sqlfMail->autoExecute('Messages', $message, 'INSERT', null, 'MessageID'); } catch (Exception $e2) { return; } } // Get recipients from database and add to ToSend table $this->addToToSendTable($messageId, $processedMessage, $mailBoxConfig); // Delete the email after successful processing $this->deleteProcessedEmail($mailbox, $emailNumber, $configName); } /** * Format email addresses from IMAP header object * @param array|object $addresses IMAP address object or array * @return string Formatted email addresses */ private function formatEmailAddresses($addresses) { if (empty($addresses)) { return ''; } $formatted = []; if (is_array($addresses)) { foreach ($addresses as $addr) { if (isset($addr->mailbox) && isset($addr->host)) { $email = $addr->mailbox . '@' . $addr->host; if (isset($addr->personal)) { $email = $addr->personal . ' <' . $email . '>'; } $formatted[] = $email; } } } return implode(', ', $formatted); } /** * Add message to ToSend table for recipients from database query * @param int $messageId Message ID * @param string $messageContent Message content * @param array $mailBoxConfig Mailbox configuration */ private function addToToSendTable($messageId, $messageContent, $mailBoxConfig) { global $sqlfMail; try { // Get recipients from database using the audience-select query $fetchRecipients = $this->sqlf->getAll($mailBoxConfig['audience-select']); $recipientCount = 0; foreach ($fetchRecipients as $recipient) { // Use Email2 if Email is too short if (strlen($recipient['Email']) <= 3 && isset($recipient['Email2']) && strlen($recipient['Email2']) > 3) { $recipient['Email'] = $recipient['Email2']; } // Skip if no valid email if (empty($recipient['Email']) || strlen($recipient['Email']) <= 3) { continue; } $sql = "INSERT INTO ToSend (SendTo, MessageID, Message) " . "VALUES ('" . addslashes($recipient['Email']) . "', " . $messageId . ", '" . addslashes($messageContent) . "')"; $sqlfMail->execute($sql); $recipientCount++; } // Update NumRecipients in Messages table if ($recipientCount > 0) { $updateSql = "UPDATE Messages SET NumRecipients = " . $recipientCount . " WHERE MessageID = " . $messageId; $sqlfMail->execute($updateSql); } } catch (Exception $e) { // ErrorLog::logInfo('Failed to add message to ToSend table: ' . $e->getMessage()); } } /** * Send error notification to sender when email contains embedded images * @param string $senderEmail Sender's email address * @param string $subject Original email subject * @param string $senderName Sender's name (optional) */ private function sendEmbeddedImageErrorNotification($senderEmail, $subject, $senderName = '') { global $sqlfMail; // Ensure DOCUMENT_ROOT is set for maill function if (!isset($_SERVER['DOCUMENT_ROOT']) || strlen($_SERVER['DOCUMENT_ROOT']) <= 0) { $_SERVER['DOCUMENT_ROOT'] = str_replace('/crons', '', getcwd()); } // Create error notification email content $emailSubject = 'Email Rejected: Embedded Images Not Supported'; $emailBody = '<html><body>'; $emailBody .= '<p>Dear ' . (!empty($senderName) ? htmlspecialchars($senderName) : 'Sender') . ',</p>'; $emailBody .= '<p>Your email has been rejected because it contains embedded images. The CESA bulk mail system does not support embedded images.</p>'; $emailBody .= '<p><strong>Email Details:</strong></p>'; $emailBody .= '<ul>'; $emailBody .= '<li><strong>Subject:</strong> ' . htmlspecialchars($subject) . '</li>'; $emailBody .= '<li><strong>Date:</strong> ' . date('Y-m-d H:i:s') . '</li>'; $emailBody .= '</ul>'; $emailBody .= '<p><strong>What you need to do:</strong></p>'; $emailBody .= '<ul>'; $emailBody .= '<li>Remove all embedded images from your email</li>'; $emailBody .= '<li>If you need to include images, attach them as regular attachments instead</li>'; $emailBody .= '<li>Resend your email without embedded images</li>'; $emailBody .= '</ul>'; $emailBody .= '<p>If you have any questions, please contact the CESA support team.</p>'; $emailBody .= '<p>Best regards,<br>CESA Mail System</p>'; $emailBody .= '</body></html>'; // Send email using maill function include_once($_SERVER['DOCUMENT_ROOT'] . '/php/includes/maill.php'); maill($senderEmail, $emailSubject, $emailBody, null, [ 'isHTML' => true, 'content-type' => 'text/html', 'from-name' => 'BCE Mail system', 'from-address' => 'website@cesa.co.za', 'reply-to-address' => 'website@cesa.co.za' ]); ErrorLog::logInfo('MailMessages: Sent embedded image error notification to ' . $senderEmail . ' for subject: ' . $subject); } /** * Send confirmation notification email to sender * @param string $senderEmail Sender's email address * @param int $messageId Message ID * @param string $confirmationToken Confirmation token * @param string $subject Original email subject * @param string $senderName Sender's name */ private function sendConfirmationNotification($senderEmail, $messageId, $confirmationToken, $subject, $senderName = '') { global $sqlfMail; // Determine base URL - try to get from config or use default $baseUrl = ''; if (isset($_SERVER['HTTP_HOST'])) { $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://'; $baseUrl = $protocol . $_SERVER['HTTP_HOST']; } else { // Fallback for cron context $baseUrl = 'https://www.cesa.co.za'; } // Ensure DOCUMENT_ROOT is set for maill function if (!isset($_SERVER['DOCUMENT_ROOT']) || strlen($_SERVER['DOCUMENT_ROOT']) <= 0) { $_SERVER['DOCUMENT_ROOT'] = str_replace('/crons', '', getcwd()); } // Build confirmation URL - use echasl23 path $confirmationUrl = $baseUrl . '/echasl23/bulkmailconfirm.php?token=' . urlencode($confirmationToken) . '&id=' . $messageId; // Create notification email content $emailSubject = 'Bulk Mail Received - Confirmation Required'; $emailBody = '<html><body>'; $emailBody .= '<p>Dear ' . (!empty($senderName) ? htmlspecialchars($senderName) : 'Sender') . ',</p>'; $emailBody .= '<p>Your bulk mail has been received and processed:</p>'; $emailBody .= '<ul>'; $emailBody .= '<li><strong>Subject:</strong> ' . htmlspecialchars($subject) . '</li>'; $emailBody .= '<li><strong>Message ID:</strong> ' . $messageId . '</li>'; $emailBody .= '</ul>'; $emailBody .= '<p>Before the message can be sent to recipients, please review and confirm that it has been processed correctly.</p>'; $emailBody .= '<p><a href="' . htmlspecialchars($confirmationUrl) . '" style="background-color: #4CAF50; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;">Review and Confirm Message</a></p>'; $emailBody .= '<p>Or copy and paste this link into your browser:</p>'; $emailBody .= '<p>' . htmlspecialchars($confirmationUrl) . '</p>'; $emailBody .= '<p>This link will allow you to:</p>'; $emailBody .= '<ul>'; $emailBody .= '<li>View the message in an HTML editor</li>'; $emailBody .= '<li>Fix any character encoding issues</li>'; $emailBody .= '<li>Review attachments</li>'; $emailBody .= '<li>Confirm that everything is correct</li>'; $emailBody .= '</ul>'; $emailBody .= '<p>Once confirmed, the message will be released for sending.</p>'; $emailBody .= '<p>Best regards,<br>CESA Mail System</p>'; $emailBody .= '</body></html>'; // Send email using maill function include_once($_SERVER['DOCUMENT_ROOT'] . '/php/includes/maill.php'); maill($senderEmail, $emailSubject, $emailBody, null, [ 'from-name' => 'CESA Mail System', 'from-address' => 'website@cesa.co.za', 'isHTML' => true, 'content-type' => 'text/html' ]); } /** * Parse email date string to MySQL datetime format * @param string $dateString Email date string * @return string MySQL datetime string */ private function parseEmailDate($dateString) { if (empty($dateString)) { return date('Y-m-d H:i:s'); } try { // Try to parse the date $timestamp = strtotime($dateString); if ($timestamp !== false) { return date('Y-m-d H:i:s', $timestamp); } } catch (Exception $e) { // ErrorLog::logInfo('Failed to parse email date: ' . $dateString . ' - ' . $e->getMessage()); } // Fallback to current date return date('Y-m-d H:i:s'); } /** * Delete a processed email from the mailbox * @param resource|IMAP\Connection $mailbox IMAP connection * @param int $emailNumber Email number * @param string $configName Configuration name */ private function deleteProcessedEmail($mailbox, $emailNumber, $configName) { try { // Mark the email for deletion $deleted = imap_delete($mailbox, $emailNumber); if ($deleted) { // ErrorLog::logInfo('Successfully marked email ' . $emailNumber . ' for deletion from mailbox ' . $configName); } else { // ErrorLog::logInfo('Failed to mark email ' . $emailNumber . ' for deletion from mailbox ' . $configName); } } catch (Exception $e) { // ErrorLog::logInfo('Exception while marking email ' . $emailNumber . ' for deletion from mailbox ' . $configName . ': ' . $e->getMessage()); } } /** * Check if an email should be processed * @param resource|IMAP\Connection $mailbox IMAP connection * @param int $emailNumber Email number * @param string $configName Configuration name * @return bool True if email should be processed */ private function shouldProcessEmail($mailbox, $emailNumber, $configName) { try { // Get email headers to check if it's valid $headers = imap_headerinfo($mailbox, $emailNumber); if (!$headers) { return false; } // Check if email has a valid subject $subject = isset($headers->subject) ? $headers->subject : ''; if (empty(trim($subject))) { return false; } // Check if email has a valid from address $from = isset($headers->from) ? $headers->from : ''; if (empty($from)) { return false; } // Extract email address from from field (can be array of objects) $fromEmail = ''; if (is_array($from) && !empty($from)) { // Handle array of from addresses - take the first one $fromObj = $from[0]; if (isset($fromObj->mailbox) && isset($fromObj->host)) { $fromEmail = $fromObj->mailbox . '@' . $fromObj->host; } } elseif (is_object($from)) { // Handle single from object if (isset($from->mailbox) && isset($from->host)) { $fromEmail = $from->mailbox . '@' . $from->host; } } elseif (is_string($from)) { // Handle string from address $fromEmail = $from; } if (empty($fromEmail)) { return false; } // Check if email is not too old (optional - you can adjust this logic) $date = isset($headers->date) ? $headers->date : ''; if (!empty($date)) { $emailTimestamp = strtotime($date); $cutoffTimestamp = strtotime('-30 days'); // Only process emails from last 30 days if ($emailTimestamp && $emailTimestamp < $cutoffTimestamp) { return false; } } // Check from address must be @cesa.co.za, or @igotafrica.com, or @igot.africa if ( strpos($fromEmail, '@cesa.co.za') === false && strpos($fromEmail, '@igotafrica.com') === false && strpos($fromEmail, '@igot.africa') === false && strpos($fromEmail, 'ottosaayman@hotmail.com') === false ) { echo "Sender not permitted \r\n"; return false; } return true; } catch (Exception $e) { // ErrorLog::logInfo('Error checking if email should be processed: ' . $e->getMessage()); return false; } } /** * Extract name from email address (e.g., "John Doe <john.doe@example.com>" -> "John Doe") * @param string $email * @return string */ private function extractNameFromEmail($email) { if (strpos($email, '<') !== false) { $parts = explode('<', $email); return trim($parts[0]); } return $email; } /** * Ensure the complete email structure is preserved * Store the raw email exactly as received to preserve MIME structure * ToSendProcess.php will handle all parsing, decoding, and extraction * @param string $rawEmail The complete raw email * @param array $parts Parsed parts for metadata (not used, kept for compatibility) * @return string The complete email message to store */ private function preserveCompleteEmailStructure($rawEmail, $parts) { // Store the raw email exactly as received // This preserves the complete MIME structure (boundaries, headers, encoding) // so that ToSendProcess.php can properly: // - Parse MIME parts using mailparse // - Identify inline images (Content-ID) vs attachments (Content-Disposition) // - Extract HTML content correctly // - Decode quoted-printable and base64 content appropriately return $rawEmail; } /** * Extract headers directly from raw email to preserve original formatting * @param string $rawEmail The complete raw email * @return string Formatted headers exactly as they appear in the original email */ private function extractRawHeaders($rawEmail) { // Find the end of headers (double CRLF or double LF) $headerEnd = strpos($rawEmail, "\r\n\r\n"); if ($headerEnd === false) { $headerEnd = strpos($rawEmail, "\n\n"); } if ($headerEnd !== false) { // Extract headers section $headers = substr($rawEmail, 0, $headerEnd); return $headers; } // Fallback: if no header separator found, return empty string return ''; } /** * Extract HTML content from raw body when normal parsing fails * @param string $body Raw email body * @return string HTML content if found, empty string otherwise */ private static function extractHtmlFromRawBody($body) { // Look for HTML content patterns in the raw body $patterns = [ // Look for HTML content between boundaries '/--[^-]+\r?\nContent-Type:\s*text\/html[^>]*\r?\n[^>]*\r?\n\r?\n(.*?)(?=\r?\n--|$)/is', // Look for HTML content with quoted-printable encoding '/--[^-]+\r?\nContent-Type:\s*text\/html[^>]*\r?\nContent-Transfer-Encoding:\s*quoted-printable[^>]*\r?\n\r?\n(.*?)(?=\r?\n--|$)/is', // Look for HTML content with base64 encoding '/--[^-]+\r?\nContent-Type:\s*text\/html[^>]*\r?\nContent-Transfer-Encoding:\s*base64[^>]*\r?\n\r?\n(.*?)(?=\r?\n--|$)/is', // Look for HTML content in general multipart structure '/Content-Type:\s*text\/html[^>]*\r?\n[^>]*\r?\n\r?\n(.*?)(?=\r?\n--|$)/is', // Look for HTML tags in the body '/(<html[^>]*>.*?<\/html>)/is', // Look for HTML body tags '/(<body[^>]*>.*?<\/body>)/is' ]; foreach ($patterns as $pattern) { if (preg_match($pattern, $body, $matches)) { $htmlContent = trim($matches[1]); // Check if this looks like valid HTML if (strlen($htmlContent) > 50 && (strpos($htmlContent, '<') !== false && strpos($htmlContent, '>') !== false)) { $htmlContent = self::robustQuotedPrintableDecode($htmlContent); return $htmlContent; } } } return ''; } }