Files
domnitor/app/Helpers/EmailHelper.php
Hosteroid 02bcc73261 Refactor email handling to use EmailHelper and auto-detect encryption
Extracted all email sending logic into a new EmailHelper class for centralized management and improved error handling. Updated AuthController, SettingsController, and EmailChannel to use EmailHelper for sending emails and logging. Added auto-detection of SMTP encryption based on port in both backend (SettingsController) and frontend (settings view), improving user experience and reducing misconfiguration. Enhanced logging for email operations and improved UI feedback for encryption selection.
2025-10-14 00:27:50 +03:00

430 lines
16 KiB
PHP

<?php
namespace App\Helpers;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
use App\Models\Setting;
use App\Services\Logger;
class EmailHelper
{
private static ?Setting $settingModel = null;
private static ?Logger $logger = null;
/**
* Get the Setting model instance
*/
private static function getSettingModel(): Setting
{
if (self::$settingModel === null) {
self::$settingModel = new Setting();
}
return self::$settingModel;
}
/**
* Get the Logger instance
*/
private static function getLogger(): Logger
{
if (self::$logger === null) {
self::$logger = new Logger('email');
}
return self::$logger;
}
/**
* Get email settings from database
*/
public static function getEmailSettings(): array
{
return self::getSettingModel()->getEmailSettings();
}
/**
* Get app settings from database
*/
public static function getAppSettings(): array
{
return self::getSettingModel()->getAppSettings();
}
/**
* Create and configure a PHPMailer instance with proper settings
*/
public static function createMailer(): PHPMailer
{
$emailSettings = self::getEmailSettings();
$mail = new PHPMailer(true);
// Server settings
$mail->isSMTP();
$mail->Host = $emailSettings['mail_host'];
$mail->SMTPAuth = !empty($emailSettings['mail_username']);
$mail->Username = $emailSettings['mail_username'];
$mail->Password = $emailSettings['mail_password'];
$mail->Port = (int)$emailSettings['mail_port'];
// Configure encryption based on port
$port = (int)$emailSettings['mail_port'];
$encryption = $emailSettings['mail_encryption'];
// Auto-detect encryption for common ports
if ($port === 465) {
// Port 465 typically uses SSL
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
} elseif ($port === 587) {
// Port 587 typically uses TLS
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
} elseif ($port === 25 || $port === 2525) {
// Port 25/2525 might use TLS or no encryption
if ($encryption === 'tls') {
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
} else {
$mail->SMTPSecure = '';
}
} else {
// Use configured encryption for other ports
if ($encryption === 'ssl') {
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
} elseif ($encryption === 'tls') {
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
} else {
$mail->SMTPSecure = '';
}
}
// Configure timeouts and SSL options
$mail->Timeout = 30; // 30 seconds total timeout
$mail->SMTPOptions = [
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true
]
];
// Set character encoding
$mail->CharSet = 'UTF-8';
return $mail;
}
/**
* Send a test email
*/
public static function sendTestEmail(string $toEmail): array
{
try {
$emailSettings = self::getEmailSettings();
$appSettings = self::getAppSettings();
$mail = self::createMailer();
// Set sender
$mail->setFrom($emailSettings['mail_from_address'], $emailSettings['mail_from_name']);
$mail->addAddress($toEmail);
// Content
$mail->isHTML(true);
$mail->Subject = 'Test Email from ' . $appSettings['app_name'];
$appName = htmlspecialchars($appSettings['app_name']);
$appUrl = htmlspecialchars($appSettings['app_url']);
$currentTime = date('F j, Y g:i A');
$mail->Body = "
<html>
<head>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #4A90E2; color: white; padding: 20px; border-radius: 5px 5px 0 0; }
.content { background: #f9f9f9; padding: 20px; border: 1px solid #ddd; }
.success { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 12px; border-radius: 4px; margin: 15px 0; }
.info-table { width: 100%; margin-top: 15px; }
.info-table td { padding: 8px; border-bottom: 1px solid #ddd; }
.info-table td:first-child { font-weight: bold; width: 150px; }
.footer { background: #333; color: white; padding: 10px; text-align: center; font-size: 12px; border-radius: 0 0 5px 5px; }
</style>
</head>
<body>
<div class='container'>
<div class='header'>
<h2>✅ Email Test Successful!</h2>
</div>
<div class='content'>
<div class='success'>
<strong>Success!</strong> Your email configuration is working correctly.
</div>
<p>This is a test email from <strong>{$appName}</strong>.</p>
<p>If you're seeing this message, it means your SMTP settings are configured properly and emails are being delivered successfully.</p>
<table class='info-table'>
<tr>
<td>SMTP Host:</td>
<td>" . htmlspecialchars($emailSettings['mail_host']) . "</td>
</tr>
<tr>
<td>SMTP Port:</td>
<td>" . htmlspecialchars($emailSettings['mail_port']) . "</td>
</tr>
<tr>
<td>Encryption:</td>
<td>" . htmlspecialchars($emailSettings['mail_encryption'] ?: 'None') . "</td>
</tr>
<tr>
<td>From Address:</td>
<td>" . htmlspecialchars($emailSettings['mail_from_address']) . "</td>
</tr>
<tr>
<td>Test Time:</td>
<td>{$currentTime}</td>
</tr>
</table>
</div>
<div class='footer'>
<p>This is an automated test message from {$appName}</p>
<p style='margin-top: 5px;'><a href='{$appUrl}' style='color: #4A90E2;'>Visit Dashboard</a></p>
</div>
</div>
</body>
</html>
";
$mail->AltBody = "Email Test Successful!\n\n" .
"This is a test email from {$appName}.\n" .
"Your SMTP configuration is working correctly.\n\n" .
"SMTP Host: {$emailSettings['mail_host']}\n" .
"SMTP Port: {$emailSettings['mail_port']}\n" .
"From: {$emailSettings['mail_from_address']}\n" .
"Test Time: {$currentTime}";
$mail->send();
return [
'success' => true,
'message' => "Test email sent successfully to {$toEmail}. Please check your inbox."
];
} catch (Exception $e) {
$errorMessage = "Failed to send test email: " . $e->getMessage();
$debugInfo = $mail->ErrorInfo ?? 'No debug info available';
// Log the error using the application's logger
self::getLogger()->error($errorMessage, [
'email' => $toEmail,
'smtp_error' => $debugInfo,
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return [
'success' => false,
'message' => $errorMessage,
'error' => $e->getMessage(),
'debug_info' => $debugInfo
];
}
}
/**
* Send a notification email
*/
public static function sendNotificationEmail(string $toEmail, string $subject, string $message, array $data = []): array
{
try {
$emailSettings = self::getEmailSettings();
$appSettings = self::getAppSettings();
$mail = self::createMailer();
// Set sender
$mail->setFrom($emailSettings['mail_from_address'], $emailSettings['mail_from_name']);
$mail->addAddress($toEmail);
// Content
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = self::formatHtmlBody($message, $data, $appSettings);
$mail->AltBody = strip_tags($message);
$mail->send();
return [
'success' => true,
'message' => "Email sent successfully to {$toEmail}"
];
} catch (Exception $e) {
$errorMessage = "Failed to send email: " . $e->getMessage();
$debugInfo = $mail->ErrorInfo ?? 'No debug info available';
// Log the error using the application's logger
self::getLogger()->error($errorMessage, [
'email' => $toEmail,
'subject' => $subject,
'smtp_error' => $debugInfo,
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return [
'success' => false,
'message' => $errorMessage,
'error' => $e->getMessage(),
'debug_info' => $debugInfo
];
}
}
/**
* Format HTML email body
*/
private static function formatHtmlBody(string $message, array $data, array $appSettings): string
{
$messageHtml = nl2br(htmlspecialchars($message));
$appName = htmlspecialchars($appSettings['app_name']);
$appUrl = htmlspecialchars($appSettings['app_url']);
// Build domain link if domain ID is available
$domainLink = '';
if (isset($data['domain_id'])) {
$domainUrl = rtrim($appUrl, '/') . '/domains/' . $data['domain_id'];
$domainLink = "<p style='margin-top: 15px;'><a href='$domainUrl' class='button'>View Domain Details</a></p>";
}
return "
<html>
<head>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #4A90E2; color: white; padding: 20px; border-radius: 5px 5px 0 0; }
.content { background: #f9f9f9; padding: 20px; border: 1px solid #ddd; }
.footer { background: #333; color: white; padding: 10px; text-align: center; font-size: 12px; border-radius: 0 0 5px 5px; }
.button { display: inline-block; padding: 10px 20px; background: #4A90E2; color: white; text-decoration: none; border-radius: 5px; margin-top: 10px; }
</style>
</head>
<body>
<div class='container'>
<div class='header'>
<h2>🔔 {$appName} Alert</h2>
</div>
<div class='content'>
<p>$messageHtml</p>
$domainLink
</div>
<div class='footer'>
<p>This is an automated message from {$appName}</p>
<p style='margin-top: 5px;'><a href='$appUrl' style='color: #4A90E2;'>Visit Dashboard</a></p>
</div>
</div>
</body>
</html>
";
}
/**
* Send verification email
*/
public static function sendVerificationEmail(string $email, string $fullName, string $token): array
{
try {
$appSettings = self::getAppSettings();
$verifyUrl = $appSettings['app_url'] . '/verify-email?token=' . $token;
$subject = 'Verify Your Email Address';
$message = "
<h2>Welcome to Domain Monitor!</h2>
<p>Hello {$fullName},</p>
<p>Thank you for registering. Please click the link below to verify your email address:</p>
<p><a href='{$verifyUrl}' style='background: #4A90E2; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;'>Verify Email Address</a></p>
<p>Or copy and paste this URL into your browser:</p>
<p>{$verifyUrl}</p>
<p>This link will expire in 24 hours.</p>
<p>If you did not create an account, please ignore this email.</p>
";
return self::sendNotificationEmail($email, $subject, $message);
} catch (\Exception $e) {
$errorMessage = "Failed to send verification email: " . $e->getMessage();
// Log the error using the application's logger
self::getLogger()->error($errorMessage, [
'email' => $email,
'full_name' => $fullName,
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return [
'success' => false,
'message' => $errorMessage,
'error' => $e->getMessage()
];
}
}
/**
* Send password reset email
*/
public static function sendPasswordResetEmail(string $email, string $fullName, string $token): array
{
try {
$appSettings = self::getAppSettings();
$resetUrl = $appSettings['app_url'] . '/reset-password?token=' . $token;
$subject = 'Reset Your Password';
$message = "
<h2>Password Reset Request</h2>
<p>Hello {$fullName},</p>
<p>We received a request to reset your password. Click the link below to create a new password:</p>
<p><a href='{$resetUrl}' style='background: #4A90E2; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;'>Reset Password</a></p>
<p>Or copy and paste this URL into your browser:</p>
<p>{$resetUrl}</p>
<p>This link will expire in 1 hour.</p>
<p>If you did not request a password reset, please ignore this email and your password will remain unchanged.</p>
";
return self::sendNotificationEmail($email, $subject, $message);
} catch (\Exception $e) {
$errorMessage = "Failed to send password reset email: " . $e->getMessage();
// Log the error using the application's logger
self::getLogger()->error($errorMessage, [
'email' => $email,
'full_name' => $fullName,
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return [
'success' => false,
'message' => $errorMessage,
'error' => $e->getMessage()
];
}
}
/**
* Get email subject based on data
*/
public static function getEmailSubject(array $data): string
{
if (isset($data['domain'])) {
$daysLeft = $data['days_left'];
if ($daysLeft <= 0) {
return "🚨 URGENT: Domain {$data['domain']} has EXPIRED";
}
if ($daysLeft == 1) {
return "⚠️ CRITICAL: Domain {$data['domain']} expires TOMORROW";
}
return "⚠️ Domain Expiration Alert: {$data['domain']} ({$daysLeft} days)";
}
return "Domain Monitor Alert";
}
}