| Current Path : /var/www/cesa.co.za/php/includes/ |
| Current File : /var/www/cesa.co.za/php/includes/Mailer.php |
<?php
/*
* Contact otto@igotafrica.com for license informatrion
*/
/**
* Help to send mails
*
* @author otto@igotafrica.com
*/
class Mailer {
public static function getMCCredentials() {
return array(
'api_key' => 'julia@xe.co.za:f47413d314ebbe6d12a07f057628b68a-us13',
);
}
/**
* Sanitize a mailbox address before validation.
* Empty check first; then strip whitespace and collapse repeated dots.
* For "Name <addr>" forms, display-name spaces are kept; only the address is cleaned.
*
* @param mixed $email
* @return string sanitized address (or "Name <addr>"), empty if nothing usable remains
*/
public static function sanitizeEmailAddress($email)
{
if (!is_string($email)) {
return '';
}
$email = trim($email);
if ($email === '') {
return '';
}
if (preg_match('/^(.*)<([^>]+)>\s*$/u', $email, $matches)) {
$name = trim($matches[1]);
$address = self::sanitizeMailboxPart(trim($matches[2]));
if ($address === '') {
return '';
}
return $name !== '' ? $name . ' <' . $address . '>' : $address;
}
return self::sanitizeMailboxPart($email);
}
/**
* Clean mailbox-only string: whitespace, SA TLD comma typos, consecutive dots.
*
* @param string $mailbox
* @return string
*/
private static function sanitizeMailboxPart($mailbox)
{
if (!is_string($mailbox)) {
return '';
}
$mailbox = trim($mailbox);
if ($mailbox === '') {
return '';
}
$mailbox = preg_replace('/[\s\x{00A0}]+/u', '', $mailbox);
if ($mailbox === null || $mailbox === '') {
return '';
}
$mailbox = self::repairSouthAfricanTldCommaTypos($mailbox);
// pamela@aseda.co..za → pamela@aseda.co.za
$mailbox = preg_replace('/\.{2,}/', '.', $mailbox);
// Trim stray dots at ends / next to @
$mailbox = trim($mailbox, '.');
$mailbox = str_replace(array('@.', '.@'), array('@', '@'), $mailbox);
if ($mailbox === '' || strpos($mailbox, '@') === false) {
return '';
}
return $mailbox;
}
/**
* Repair SA TLD typos where a comma was typed instead of a dot before the country code.
* Example: user@globtek.co,za → user@globtek.co.za
*
* @param mixed $value
* @return string
*/
public static function repairSouthAfricanTldCommaTypos($value)
{
if (!is_string($value) || $value === '') {
return is_string($value) ? $value : '';
}
return preg_replace('/\.(co|org|net|gov|ac|edu),za\b/i', '.$1.za', $value);
}
/**
* Whether the value is a syntactically valid mailbox address.
* Sanitize first, then reject empty, then validate.
*
* @param mixed $email
* @return bool
*/
public static function isValidEmailAddress($email)
{
if (!is_string($email)) {
return false;
}
// Empty check first (before heavier sanitize work on named forms)
if (trim($email) === '') {
return false;
}
$mailbox = self::sanitizeEmailAddress($email);
if (strpos($mailbox, '<') !== false) {
if (preg_match('/<([^>]+)>/', $mailbox, $matches)) {
$mailbox = self::sanitizeMailboxPart($matches[1]);
} else {
return false;
}
}
if ($mailbox === '' || strpos($mailbox, '@') === false) {
return false;
}
return filter_var($mailbox, FILTER_VALIDATE_EMAIL) !== false;
}
/**
* Split a recipient string or list into unique non-empty raw parts after TLD typo repair.
* Empty / whitespace-only segments (e.g. from "a@b.com,,c@d.com") are dropped.
*
* @param mixed $to
* @return array
*/
public static function parseRecipientList($to)
{
if (is_array($to)) {
$merged = array();
foreach ($to as $item) {
$merged = array_merge($merged, self::parseRecipientList($item));
}
return array_values(array_unique($merged));
}
if (!is_string($to) && !is_numeric($to)) {
return array();
}
$to = self::repairSouthAfricanTldCommaTypos((string) $to);
$parts = preg_split('/\s*[,;]\s*/', $to, -1, PREG_SPLIT_NO_EMPTY);
if (!is_array($parts)) {
return array();
}
$result = array();
foreach ($parts as $part) {
$part = trim($part);
// Also treat NBSP-only / whitespace-only as empty
$stripped = preg_replace('/[\s\x{00A0}]+/u', '', $part);
if ($part === '' || $stripped === null || $stripped === '') {
continue;
}
$result[] = $part;
}
return array_values(array_unique($result));
}
/**
* Sanitize and validate a recipient; returns mailbox only, or empty if invalid.
* Optional by-ref $displayName is filled when input is "Name <addr>".
*
* @param mixed $email
* @param string|null $displayName
* @return string
*/
public static function normalizeEmailAddress($email, &$displayName = null)
{
$displayName = '';
if (!is_string($email) || trim($email) === '') {
return '';
}
$sanitized = self::sanitizeEmailAddress($email);
if ($sanitized === '') {
return '';
}
$mailbox = $sanitized;
if (strpos($sanitized, '<') !== false) {
$rcptArr = explode('<', $sanitized, 2);
$displayName = trim($rcptArr[0]);
$mailbox = self::sanitizeEmailAddress(str_replace('>', '', $rcptArr[1]));
}
// Sanitize already applied; empty check then validate
if ($mailbox === '' || !self::isValidEmailAddress($mailbox)) {
$displayName = '';
return '';
}
return $mailbox;
}
public static function extractHeaders($rawHeaders) {
$lines = explode("\n", $rawHeaders);
$return = array();
foreach ($lines as $line) {
$lineData = explode(': ', $line);
if ($lineData[0] == 'From') {
$nameFrom = explode('<', $lineData[1]);
if (count($nameFrom) > 0) {
$return['from-name'] = trim($nameFrom[0]);
$return['from-address'] = str_replace('>', '', $nameFrom[1]);
} else {
$return['from-address'] = $lineData[1];
}
} elseif ($lineData[0] == 'Reply-To') {
$return['reply-to-address'] = $lineData[1];
} elseif ($lineData[0] == 'Content-Type') {
$return['content-type'] = trim(substr($rawHeaders, strpos($rawHeaders, 'Content-Type') + 13));
} elseif ($lineData[0] == 'Content-Type') {
$return['confirm-read'] = true;
}
}
return $return;
}
/**
* Escape text for iCalendar (RFC 5545): backslash, semicolon, comma, newline.
* Outlook and other clients require this for SUMMARY, DESCRIPTION, LOCATION.
*/
public static function escapeIcalText($value) {
if ($value === null || $value === '') {
return '';
}
return str_replace(array('\\', ';', ',', "\r\n", "\n", "\r"), array('\\\\', '\\;', '\\,', '\\n', '\\n', '\\n'), $value);
}
/**
* Build RFC 5545 / Outlook-compatible .ics calendar invite.
* Event times are interpreted as South Africa (Africa/Johannesburg, UTC+2)
* and converted to UTC in the .ics so Outlook and other clients show the correct local time.
*/
public static function getCalendarInviteUid($marketingEvent, $eventAttendee)
{
$eventId = isset($marketingEvent['EventID']) ? intval($marketingEvent['EventID']) : 0;
$attendeeId = isset($eventAttendee['AttendeeID']) ? intval($eventAttendee['AttendeeID']) : 0;
if ($eventId > 0 && $attendeeId > 0) {
return 'marketing-event-' . $eventId . '-attendee-' . $attendeeId . '@cesa.co.za';
}
return md5(uniqid(mt_rand(), true)) . '@cesa.co.za';
}
public static function getICalText($marketingEvent, $eventAttendee, $options = array()) {
$isUpdate = !empty($options['isUpdate']);
$changeMessage = isset($options['changeMessage']) ? trim((string) $options['changeMessage']) : '';
$sequence = isset($options['sequence']) ? intval($options['sequence']) : ($isUpdate ? time() : 0);
$uid = isset($options['uid']) && strlen($options['uid']) > 0
? $options['uid']
: self::getCalendarInviteUid($marketingEvent, $eventAttendee);
$tzSA = new \DateTimeZone('Africa/Johannesburg');
$tzUTC = new \DateTimeZone('UTC');
$startDateTime = new \DateTime($marketingEvent['StartDate'] . ' ' . $marketingEvent['StartTime'], $tzSA);
$endDateTime = new \DateTime($marketingEvent['EndDate'] . ' ' . $marketingEvent['EndTime'], $tzSA);
$startDateTime->setTimezone($tzUTC);
$endDateTime->setTimezone($tzUTC);
$venue = isset($marketingEvent['Venue']) ? $marketingEvent['Venue'] : '';
if (isset($marketingEvent['City']) && strlen($marketingEvent['City']) > 0) {
$venue .= ($venue !== '' ? ', ' : '') . $marketingEvent['City'];
}
if (isset($marketingEvent['Province']) && strlen($marketingEvent['Province']) > 0) {
$venue .= ($venue !== '' ? ', ' : '') . $marketingEvent['Province'];
}
$summary = isset($marketingEvent['EventName']) ? $marketingEvent['EventName'] : '';
$description = isset($marketingEvent['EventDescription']) ? $marketingEvent['EventDescription'] : '';
if ($isUpdate) {
$description = 'This event has been updated.' . ($description !== '' ? "\n\n" . $description : '');
}
if ($changeMessage !== '') {
$description .= ($description !== '' ? "\n\n" : '') . $changeMessage;
}
$attendeeName = trim(($eventAttendee['ContactFirstName'] ?? '') . ' ' . ($eventAttendee['ContactLastName'] ?? ''));
$text = "BEGIN:VCALENDAR\r\n"
. "VERSION:2.0\r\n"
. "PRODID:-//CESA//events/NONSGML v1.0//EN\r\n"
. "CALSCALE:GREGORIAN\r\n"
. "METHOD:REQUEST\r\n"
. "BEGIN:VTIMEZONE\r\n"
. "TZID:Africa/Johannesburg\r\n"
. "BEGIN:STANDARD\r\n"
. "DTSTART:19700101T000000\r\n"
. "TZOFFSETFROM:+0200\r\n"
. "TZOFFSETTO:+0200\r\n"
. "TZNAME:SAST\r\n"
. "END:STANDARD\r\n"
. "END:VTIMEZONE\r\n"
. "BEGIN:VTIMEZONE\r\n"
. "TZID:UTC\r\n"
. "BEGIN:STANDARD\r\n"
. "DTSTART:19700101T000000Z\r\n"
. "TZOFFSETFROM:+0000\r\n"
. "TZOFFSETTO:+0000\r\n"
. "TZNAME:UTC\r\n"
. "END:STANDARD\r\n"
. "END:VTIMEZONE\r\n"
. "BEGIN:VEVENT\r\n"
. "UID:" . $uid . "\r\n"
. "SEQUENCE:" . $sequence . "\r\n"
. "DTSTAMP:" . gmdate('Ymd\THis\Z') . "\r\n"
. "DTSTART:" . $startDateTime->format('Ymd\THis\Z') . "\r\n"
. "DTEND:" . $endDateTime->format('Ymd\THis\Z') . "\r\n"
. "SUMMARY:" . self::escapeIcalText($summary) . "\r\n"
. "ORGANIZER;CN=CESA:mailto:events@cesa.co.za\r\n"
. "LOCATION:" . self::escapeIcalText($venue) . "\r\n"
. "DESCRIPTION:" . self::escapeIcalText($description) . "\r\n";
if (isset($marketingEvent['BannerImgURL']) && strlen($marketingEvent['BannerImgURL']) > 0) {
$eventBanner = @file_get_contents($marketingEvent['BannerImgURL']);
if ($eventBanner !== false && strlen($eventBanner) > 0) {
$fileName = basename($marketingEvent['BannerImgURL']);
$text .= "ATTACH;ENCODING=BASE64;VALUE=BINARY;X-FILENAME=" . $fileName . ":" . base64_encode($eventBanner) . "\r\n";
}
}
if (false) {
// Add URL for event later
$text .= "URL;VALUE=URI:https://www.redacted.com" . "\r\n";
}
$attendeeCn = $attendeeName !== '' ? ';CN="' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $attendeeName) . '"' : '';
$text .= "ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE" . $attendeeCn . ";X-NUM-GUESTS=0:MAILTO:" . $eventAttendee['EmailAddress'] . "\r\n"
. "END:VEVENT\r\n"
. "END:VCALENDAR\r\n";
return $text;
}
}