WizdomWeb/app/Utilities/EmailUtility.php

394 lines
15 KiB
PHP

<?php
namespace WizdomNetworks\WizeWeb\Utilities;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
use PHPMAILER\PHPMAILER\SMTP;
use WizdomNetworks\WizeWeb\Utilities\Logger;
use WizdomNetworks\WizeWeb\Utilities\Database;
class EmailUtility
{
private static QueueUtility $queueUtility;
/**
* Initialize the EmailUtility with the QueueUtility instance.
*
* @param QueueUtility $queueUtility The queue utility instance for managing email queues.
*/
public static function initialize(QueueUtility $queueUtility): void
{
self::$queueUtility = $queueUtility;
}
/**
* Retrieve and validate email configuration from environment variables.
*
* @return array The validated email configuration settings.
* @throws \RuntimeException If required email configurations are missing.
*/
private static function getConfig(): array
{
if ( $_ENV['SMTP_AUTH'] === false ) {
$config = [
'host' => $_ENV['SMTP_HOST'] ?? 'localhost',
'port' => $_ENV['SMTP_PORT'] ?? '25',
'from_email' => $_ENV['SMTP_FROM_EMAIL'] ?? 'concierge@helpdeskplus.ca',
'from_name' => $_ENV['SMTP_FROM_NAME'] ?? 'HelpDesk+',
'auth' => false
];
foreach (['host', 'port', 'from_email'] as $field) {
if (empty($config[$field])) {
Logger::logError("Missing email configuration: $field");
throw new \RuntimeException("Missing email configuration: $field");
}
}
}
else {
$config = [
'host' => $_ENV['SMTP_HOST'] ?? 'localhost',
'username' => $_ENV['SMTP_USERNAME'] ?? null,
'password' => $_ENV['SMTP_PASSWORD'] ?? null,
'port' => $_ENV['SMTP_PORT'] ?? '25',
'encryption' => $_ENV['SMTP_ENCRYPTION'] ?? 'none',
'from_email' => $_ENV['SMTP_FROM_EMAIL'] ?? 'concierge@helpdeskplus.ca',
'from_name' => $_ENV['SMTP_FROM_NAME'] ?? 'HelpDesk+',
'smtpsecure' => $_ENV['SMTP_AUTH'],
'auth' => true,
'autotls' => true
];
foreach (['host', 'port', 'username', 'password', 'from_email'] as $field) {
if (empty($config[$field])) {
Logger::logError("Missing email configuration: $field");
throw new \RuntimeException("Missing email configuration: $field");
}
}
}
return $config;
}
/**
* Render an email template with dynamic data.
*
* @param string $templatePath The path to the email template file.
* @param array $data Key-value pairs for template placeholders.
* @return string The rendered email content.
*/
public static function renderTemplate(string $templatePath, array $data): string
{
if (!file_exists($templatePath)) {
Logger::logError("Email template not found: $templatePath");
return '';
}
$content = file_get_contents($templatePath);
foreach ($data as $key => $value) {
$content = str_replace("{{{$key}}}", $value, $content);
}
return $content;
}
/**
* Log email status into the database.
*
* @param string $recipient The recipient email address.
* @param string $status The status of the email (e.g., 'queued', 'sent', 'failed').
* @param string|null $errorMessage An optional error message.
* @return void
*/
private static function logEmailStatus(string $recipient, string $status, ?string $errorMessage = null): void
{
try {
$db = Database::getInstance();
$query = "INSERT INTO email_status (recipient, status, error_message, created_at) VALUES (:recipient, :status, :error_message, NOW())";
$params = [
':recipient' => $recipient,
':status' => $status,
':error_message' => $errorMessage,
];
$db->executeQuery($query, $params);
} catch (\Throwable $e) {
Logger::logError("Failed to log email status: " . $e->getMessage());
}
}
/**
* Notify admin or sales team via email.
*
* @param string $emailType The type of notification (e.g., 'admin', 'sales').
* @param string $subject The email subject.
* @param string $templatePath Path to the notification template.
* @param array $templateData Data for the template placeholders.
* @return void
*/
public static function notifyTeam(string $emailType, string $subject, string $templatePath, array $templateData): void
{
$recipients = $emailType === 'admin' ? explode(',', $_ENV['ADMIN_EMAILS']) : explode(',', $_ENV['SALES_EMAILS']);
foreach ($recipients as $recipient) {
$recipient = trim($recipient);
if (!self::sendEmail($recipient, $subject, $templatePath, $templateData)) {
Logger::logError("Failed to send $emailType notification to: $recipient");
}
}
}
/**
* Send an email with enhanced error categorization.
*
* @param string $recipient Recipient email address.
* @param string $subject Email subject.
* @param string $templatePath Path to the email template.
* @param array $templateData Data to replace placeholders in the template.
* @param array $options Optional configurations (e.g., CC, BCC).
* @param int $retryLimit The maximum number of retries for transient failures.
* @return bool Returns true on success, false otherwise.
*/
public static function sendEmail(string $recipient, string $subject, string $templatePath, array $templateData = [], array $options = [], int $retryLimit = 3): bool
{
$mail = new PHPMailer(true);
$config = self::getConfig();
$retryCount = 0;
while ($retryCount <= $retryLimit) {
try {
$mail->isSMTP();
$mail->SMTPAutoTLS = false;
$mail->SMTPAuth = false;
/* If authentication is enabled setup the connection */
if ( $config['auth'] === 'true' ){
$mail->SMTPAuth = $config['auth'];
$mail->Username = $config['username'];
$mail->Password = $config['password'];
$mail->SMTPSecure = $config['encryption'];
$mail->SMTPAutoTLS = $config['autotls'];
}
$mail->Host = $config['host'];
$mail->Port = $config['port'];
/******************************
$mail->SMTPDebug = $_ENV['APP_ENV'] === 'development' ? 2 : 0;
/$mail->Debugoutput = function ($message, $level) {
Logger::logInfo("SMTP Debug [$level]: $message");
};
*******************************/
$mail->setFrom($config['from_email'], $config['from_name']);
$mail->addAddress($recipient);
if (!empty($options['cc'])) {
foreach ((array)$options['cc'] as $cc) {
$mail->addCC($cc);
}
}
if (!empty($options['bcc'])) {
foreach ((array)$options['bcc'] as $bcc) {
$mail->addBCC($bcc);
}
}
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = self::renderTemplate($templatePath, $templateData);
$mail->send();
Logger::logInfo("Email sent to $recipient with subject: $subject");
self::logEmailStatus($recipient, 'sent');
return true;
} catch (Exception $e) {
$retryCount++;
$error = $mail->ErrorInfo;
Logger::logWarning("Email send failed for $recipient (Attempt $retryCount/$retryLimit): $error");
if (str_contains($error, '452 4.3.1')) {
Logger::logWarning("Transient error detected for $recipient: $error");
} elseif (str_contains($error, '550')) {
Logger::logError("Permanent error detected for $recipient: $error");
self::logEmailStatus($recipient, 'failed', $error);
return false;
} elseif (str_contains($error, '421')) {
Logger::logWarning("Rate-limiting error detected for $recipient: $error");
} else {
Logger::logError("Unhandled SMTP error for $recipient: $error");
}
if (str_contains($error, '452') || str_contains($error, '421')) {
if ($retryCount > $retryLimit) {
Logger::logError("Exceeded retry limit for email to $recipient: $error");
self::logEmailStatus($recipient, 'failed', $error);
return false;
}
sleep(5);
continue;
}
Logger::logError("Email permanently failed for $recipient: $error");
self::logEmailStatus($recipient, 'failed', $error);
return false;
}
}
return false;
}
/**
* Process the email queue and send emails in batches.
*
* @param int $batchSize Number of emails to process in a single batch.
* @param int $maxRetries Maximum retry attempts for failed emails.
* @return void
*/
public static function processEmailQueue(int $batchSize = 10, int $maxRetries = 3): void
{
for ($i = 0; $i < $batchSize; $i++) {
$emailData = self::$queueUtility->dequeue('email');
if ($emailData === null) {
Logger::logInfo("No more emails to process in the queue.");
break;
}
$success = self::sendEmail(
$emailData['recipient'],
$emailData['subject'],
$emailData['templatePath'],
$emailData['templateData'],
$emailData['options']
);
if (!$success) {
$retries = $emailData['retries'] ?? 0;
if ($retries < $maxRetries) {
$priority = $emailData['priority'] ?? 0;
$emailData['retries'] = $retries + 1;
self::$queueUtility->enqueue('email', $emailData, $priority);
Logger::logWarning(
"Email re-queued for recipient: {$emailData['recipient']} (Attempt {$emailData['retries']})"
);
} else {
Logger::logError("Email permanently failed for recipient: {$emailData['recipient']}");
self::logEmailStatus($emailData['recipient'], 'failed', 'Max retry limit reached.');
}
} else {
self::logEmailStatus($emailData['recipient'], 'sent');
}
}
Logger::logInfo("Email queue processing completed.");
}
/**
* Process contact-related email queue.
*
* @param int $batchSize Number of emails to process in a single batch.
* @param int $maxRetries Maximum retry attempts for failed emails.
* @return void
*/
public static function processContactQueue(int $batchSize = 10, int $maxRetries = 3): void
{
Logger::logInfo("Processing contact email queue...");
for ($i = 0; $i < $batchSize; $i++) {
$emailData = self::$queueUtility->dequeue('contact_email');
if ($emailData === null) {
Logger::logInfo("No more emails to process in the contact queue.");
break;
}
$success = self::sendEmail(
$emailData['recipient'],
$emailData['subject'],
$emailData['templatePath'],
$emailData['templateData'],
$emailData['options']
);
if (!$success) {
$retries = $emailData['retries'] ?? 0;
if ($retries < $maxRetries) {
$priority = $emailData['priority'] ?? 0;
$emailData['retries'] = $retries + 1;
self::$queueUtility->enqueue('contact_email', $emailData, $priority);
Logger::logWarning(
"Contact email re-queued for recipient: {$emailData['recipient']} (Attempt {$emailData['retries']})"
);
} else {
Logger::logError("Contact email permanently failed for recipient: {$emailData['recipient']}");
self::logEmailStatus($emailData['recipient'], 'failed', 'Max retry limit reached.');
}
} else {
self::logEmailStatus($emailData['recipient'], 'sent');
}
}
Logger::logInfo("Contact email queue processing completed.");
}
/**
* Retrieve the status of a specific email by recipient.
*
* @param string $recipient Email address of the recipient.
* @return array|null The email status or null if not found.
*/
public static function getEmailStatus(string $recipient): ?array
{
try {
$db = Database::getInstance();
$query = "SELECT * FROM email_status WHERE recipient = :recipient ORDER BY created_at DESC LIMIT 1";
$params = [':recipient' => $recipient];
return $db->fetchOne($query, $params);
} catch (\Throwable $e) {
Logger::logError("Failed to retrieve email status for $recipient: " . $e->getMessage());
return null;
}
}
/**
* Clear the email queue.
*
* @param string $queueName The name of the queue to clear (default: 'email').
* @return void
*/
public static function clearQueue(string $queueName = 'email'): void
{
Logger::logInfo("Clearing queue: $queueName");
try {
self::$queueUtility->clearQueue($queueName);
Logger::logInfo("Queue $queueName cleared successfully.");
} catch (\Throwable $e) {
Logger::logError("Failed to clear queue $queueName: " . $e->getMessage());
}
}
/**
* List all queued emails in a specific queue.
*
* @param string $queueName The name of the queue to inspect (default: 'email').
* @return array List of queued emails.
*/
public static function listQueuedEmails(string $queueName = 'email'): array
{
try {
Logger::logInfo("Listing emails in queue: $queueName");
return self::$queueUtility->listQueue($queueName);
} catch (\Throwable $e) {
Logger::logError("Failed to list emails in queue $queueName: " . $e->getMessage());
return [];
}
}
}