394 lines
15 KiB
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 [];
|
|
}
|
|
}
|
|
}
|