38451-vm/mail/MailService.php
Flatlogic Bot 4f543a3144 完美了
2026-03-01 07:18:56 +00:00

300 lines
15 KiB
PHP

<?php
// Minimal mail service for the workspace app (VM).
// Usage:
// require_once __DIR__ . '/MailService.php';
// // Generic:
// MailService::sendMail($to, $subject, $htmlBody, $textBody = null, $opts = []);
// // Contact form helper:
// MailService::sendContactMessage($name, $email, $message, $to = null, $subject = 'New contact form');
class MailService
{
// Universal mail sender (no attachments by design)
public static function sendMail($to, string $subject, string $htmlBody, ?string $textBody = null, array $opts = [])
{
$cfg = self::loadConfig();
$autoload = __DIR__ . '/../vendor/autoload.php';
if (file_exists($autoload)) {
require_once $autoload;
}
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
// Local PHPMailer (Priority for portability)
require_once __DIR__ . '/PHPMailer/src/Exception.php';
require_once __DIR__ . '/PHPMailer/src/SMTP.php';
require_once __DIR__ . '/PHPMailer/src/PHPMailer.php';
}
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
return [ 'success' => false, 'error' => 'PHPMailer not available' ];
}
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = $cfg['smtp_host'] ?? '';
$mail->Port = (int)($cfg['smtp_port'] ?? 587);
$secure = $cfg['smtp_secure'] ?? 'tls';
if ($secure === 'ssl') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS;
elseif ($secure === 'tls') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
else $mail->SMTPSecure = false;
$mail->SMTPAuth = true;
$mail->Username = $cfg['smtp_user'] ?? '';
$mail->Password = $cfg['smtp_pass'] ?? '';
$fromEmail = $opts['from_email'] ?? ($cfg['from_email'] ?? 'no-reply@localhost');
$fromName = $opts['from_name'] ?? ($cfg['from_name'] ?? 'App');
$mail->setFrom($fromEmail, $fromName);
if (!empty($opts['reply_to']) && filter_var($opts['reply_to'], FILTER_VALIDATE_EMAIL)) {
$mail->addReplyTo($opts['reply_to']);
} elseif (!empty($cfg['reply_to'])) {
$mail->addReplyTo($cfg['reply_to']);
}
// Recipients
$toList = [];
if ($to) {
if (is_string($to)) $toList = array_map('trim', explode(',', $to));
elseif (is_array($to)) $toList = $to;
} elseif (!empty(getenv('MAIL_TO'))) {
$toList = array_map('trim', explode(',', getenv('MAIL_TO')));
}
$added = 0;
foreach ($toList as $addr) {
if (filter_var($addr, FILTER_VALIDATE_EMAIL)) { $mail->addAddress($addr); $added++; }
}
if ($added === 0) {
return [ 'success' => false, 'error' => 'No recipients defined (set MAIL_TO or pass $to)' ];
}
foreach ((array)($opts['cc'] ?? []) as $cc) { if (filter_var($cc, FILTER_VALIDATE_EMAIL)) $mail->addCC($cc); }
foreach ((array)($opts['bcc'] ?? []) as $bcc){ if (filter_var($bcc, FILTER_VALIDATE_EMAIL)) $mail->addBCC($bcc); }
// Optional DKIM
if (!empty($cfg['dkim_domain']) && !empty($cfg['dkim_selector']) && !empty($cfg['dkim_private_key_path'])) {
$mail->DKIM_domain = $cfg['dkim_domain'];
$mail->DKIM_selector = $cfg['dkim_selector'];
$mail->DKIM_private = $cfg['dkim_private_key_path'];
}
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $htmlBody;
$mail->AltBody = $textBody ?? strip_tags($htmlBody);
$ok = $mail->send();
return [ 'success' => $ok ];
} catch (\Throwable $e) {
return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ];
}
}
/**
* Send verification code email (BYRO Style)
*/
public static function sendVerificationCode(string $to, string $code, string $type = 'register')
{
$subject = ($type === 'register' ? '[BYRO] Create Account' : '[BYRO] Reset Password') . ' - Verification Code: ' . $code;
$title = $type === 'register' ? 'Verify your email' : 'Reset your password';
$instruction = $type === 'register' ? 'You are creating an account on BYRO.' : 'You are resetting your account password.';
$action = $type === 'register' ? 'registration' : 'password reset';
$html = "
<div style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 0; color: #1e2329; background-color: #fff; border: 1px solid #eaecef; border-radius: 16px; overflow: hidden;\">
<div style=\"padding: 32px 40px; background-color: #000; color: #fff;\">
<div style=\"font-size: 28px; font-weight: 900; letter-spacing: -1px;\">BYRO</div>
</div>
<div style=\"padding: 40px;\">
<h2 style=\"font-size: 24px; font-weight: 700; margin-bottom: 24px; color: #000; margin-top: 0;\">{$title}</h2>
<p style=\"font-size: 16px; line-height: 24px; color: #1e2329; margin-bottom: 24px;\">
Dear user,<br><br>
{$instruction} Please use the following 6-digit verification code to complete your {$action}.
</p>
<div style=\"background-color: #f5f5f5; border-radius: 12px; padding: 32px; text-align: center; margin-bottom: 32px;\">
<span style=\"font-family: 'Roboto Mono', monospace; font-size: 42px; font-weight: 700; letter-spacing: 12px; color: #000;\">{$code}</span>
</div>
<p style=\"font-size: 14px; line-height: 20px; color: #474d57; margin-bottom: 32px;\">
This code is valid for <strong>10 minutes</strong>. For your account security, please do not share this code with anyone, including BYRO staff.
</p>
<div style=\"background-color: #fff9e6; border-left: 4px solid #f0b90b; padding: 20px; margin-bottom: 32px;\">
<h4 style=\"margin: 0 0 8px 0; color: #d49e00; font-size: 14px;\">Safety Warning:</h4>
<ul style=\"margin: 0; padding-left: 20px; font-size: 13px; color: #707a8a; line-height: 1.6;\">
<li>Never give your password or verification code to anyone.</li>
<li>Always check the website URL to ensure you are on the official BYRO platform.</li>
<li>Enable Two-Factor Authentication (2FA) for enhanced security.</li>
</ul>
</div>
<p style=\"font-size: 14px; line-height: 20px; color: #707a8a; margin-bottom: 0;\">
If you did not initiate this request, please change your password immediately and contact our customer support.
</p>
</div>
<div style=\"padding: 32px 40px; background-color: #fafafa; border-top: 1px solid #eaecef; text-align: center;\">
<p style=\"font-size: 12px; line-height: 18px; color: #707a8a; margin-bottom: 12px;\">
This is an automated message, please do not reply.
</p>
<div style=\"font-size: 12px; font-weight: 700; color: #000; margin-bottom: 8px;\">BYRO Team</div>
<div style=\"font-size: 11px; color: #b7bdc6;\">&copy; 2026 BYRO. All rights reserved.</div>
</div>
</div>
";
$text = "{$title}\n\nDear user,\n\n{$instruction} Your verification code is: {$code}\n\nThis code is valid for 10 minutes. For your account security, do not share this code with anyone.\n\nSafety Tips:\n- Never give your password or verification code to anyone.\n- Ensure you are on the official BYRO website.\n\nBYRO Team";
return self::sendMail($to, $subject, $html, $text);
}
private static function loadConfig(): array
{
// Load default from file
$configPath = __DIR__ . '/config.php';
$cfg = file_exists($configPath) ? require $configPath : [];
// Override with database settings if available
try {
require_once __DIR__ . '/../db/config.php';
$stmt = db()->query("SELECT setting_key, setting_value FROM system_settings WHERE setting_key LIKE 'smtp_%' OR setting_key LIKE 'mail_%'");
$db_settings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
if (!empty($db_settings['smtp_host'])) $cfg['smtp_host'] = $db_settings['smtp_host'];
if (!empty($db_settings['smtp_port'])) $cfg['smtp_port'] = (int)$db_settings['smtp_port'];
if (!empty($db_settings['smtp_user'])) $cfg['smtp_user'] = $db_settings['smtp_user'];
if (!empty($db_settings['smtp_pass'])) $cfg['smtp_pass'] = $db_settings['smtp_pass'];
if (!empty($db_settings['smtp_secure'])) $cfg['smtp_secure'] = $db_settings['smtp_secure'];
if (!empty($db_settings['mail_from_email'])) $cfg['from_email'] = $db_settings['mail_from_email'];
if (!empty($db_settings['mail_from_name'])) $cfg['from_name'] = $db_settings['mail_from_name'];
} catch (\Throwable $e) {
// Fallback to environment if DB fails
}
return $cfg;
}
// Send a contact message
// $to can be: a single email string, a comma-separated list, an array of emails, or null (fallback to MAIL_TO/MAIL_FROM)
public static function sendContactMessage(string $name, string $email, string $message, $to = null, string $subject = 'New contact form')
{
$cfg = self::loadConfig();
// Try Composer autoload if available (for PHPMailer)
$autoload = __DIR__ . '/../vendor/autoload.php';
if (file_exists($autoload)) {
require_once $autoload;
}
// Fallback to system-wide PHPMailer (installed via apt: libphp-phpmailer)
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
// Debian/Ubuntu package layout (libphp-phpmailer)
@require_once 'libphp-phpmailer/autoload.php';
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
@require_once 'libphp-phpmailer/src/Exception.php';
@require_once 'libphp-phpmailer/src/SMTP.php';
@require_once 'libphp-phpmailer/src/PHPMailer.php';
}
// Alternative layout (older PHPMailer package names)
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
@require_once 'PHPMailer/src/Exception.php';
@require_once 'PHPMailer/src/SMTP.php';
@require_once 'PHPMailer/src/PHPMailer.php';
}
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
@require_once 'PHPMailer/Exception.php';
@require_once 'PHPMailer/SMTP.php';
@require_once 'PHPMailer/PHPMailer.php';
}
}
$transport = $cfg['transport'] ?? 'smtp';
if ($transport === 'smtp' && class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
return self::sendViaPHPMailer($cfg, $name, $email, $message, $to, $subject);
}
// Fallback: attempt native mail() — works only if MTA is configured on the VM
return self::sendViaNativeMail($cfg, $name, $email, $message, $to, $subject);
}
private static function sendViaPHPMailer(array $cfg, string $name, string $email, string $body, $to, string $subject)
{
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = $cfg['smtp_host'] ?? '';
$mail->Port = (int)($cfg['smtp_port'] ?? 587);
$secure = $cfg['smtp_secure'] ?? 'tls';
if ($secure === 'ssl') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS;
elseif ($secure === 'tls') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
else $mail->SMTPSecure = false;
$mail->SMTPAuth = true;
$mail->Username = $cfg['smtp_user'] ?? '';
$mail->Password = $cfg['smtp_pass'] ?? '';
$fromEmail = $cfg['from_email'] ?? 'no-reply@localhost';
$fromName = $cfg['from_name'] ?? 'App';
$mail->setFrom($fromEmail, $fromName);
// Use Reply-To for the user's email to avoid spoofing From
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
$mail->addReplyTo($email, $name ?: $email);
}
if (!empty($cfg['reply_to'])) {
$mail->addReplyTo($cfg['reply_to']);
}
// Destination: prefer dynamic recipients ($to), fallback to MAIL_TO; no silent FROM fallback
$toList = [];
if ($to) {
if (is_string($to)) {
// allow comma-separated list
$toList = array_map('trim', explode(',', $to));
} elseif (is_array($to)) {
$toList = $to;
}
} elseif (!empty(getenv('MAIL_TO'))) {
$toList = array_map('trim', explode(',', getenv('MAIL_TO')));
}
$added = 0;
foreach ($toList as $addr) {
if (filter_var($addr, FILTER_VALIDATE_EMAIL)) {
$mail->addAddress($addr);
$added++;
}
}
if ($added === 0) {
return [ 'success' => false, 'error' => 'No recipients defined (set MAIL_TO or pass $to)' ];
}
// DKIM (optional)
if (!empty($cfg['dkim_domain']) && !empty($cfg['dkim_selector']) && !empty($cfg['dkim_private_key_path'])) {
$mail->DKIM_domain = $cfg['dkim_domain'];
$mail->DKIM_selector = $cfg['dkim_selector'];
$mail->DKIM_private = $cfg['dkim_private_key_path'];
}
$mail->isHTML(true);
$mail->Subject = $subject;
$safeName = htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$safeEmail = htmlspecialchars($email, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$safeBody = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
$mail->Body = "<p><strong>Name:</strong> {$safeName}</p><p><strong>Email:</strong> {$safeEmail}</p><hr>{$safeBody}";
$mail->AltBody = "Name: {$name}\nEmail: {$email}\n\n{$body}";
$ok = $mail->send();
return [ 'success' => $ok ];
} catch (\Throwable $e) {
return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ];
}
}
private static function sendViaNativeMail(array $cfg, string $name, string $email, string $body, $to, string $subject)
{
$opts = ['reply_to' => $email];
$html = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
return self::sendMail($to, $subject, $html, $body, $opts);
}
}