false]; } $data = json_decode(file_get_contents(MAIL_CONFIG_FILE) ?: '{}', true); $config = is_array($data) ? $data : ['enabled' => false]; return $config; } function getMailRecipients(array $mailConfig): array { $recipients = []; if (!empty($mailConfig['recipients']) && is_array($mailConfig['recipients'])) { foreach ($mailConfig['recipients'] as $email) { $email = trim((string) $email); if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL)) { $recipients[] = $email; } } } $single = trim((string) ($mailConfig['recipient'] ?? '')); if ($single !== '' && filter_var($single, FILTER_VALIDATE_EMAIL)) { $recipients[] = $single; } return array_values(array_unique($recipients)); } function formatSubmissionTime(string $iso): string { $ts = strtotime($iso); if ($ts === false) { return $iso; } return date('Y-m-d H:i:s', $ts); } function sendSubmissionMail(array $allSubmissions): bool { $mailConfig = loadMailConfig(); if (empty($mailConfig['enabled'])) { return false; } $recipients = getMailRecipients($mailConfig); $smtp = $mailConfig['smtp'] ?? []; if ($recipients === [] || empty($smtp['host'])) { error_log('[mail] 邮件配置不完整'); return false; } $count = count($allSubmissions); $subject = (string) ($mailConfig['subject'] ?? '【心理咨询】新预约提交'); if ($count > 0) { $subject .= '(共' . $count . '条)'; } $htmlBody = buildAllSubmissionsMailHtml($allSubmissions); $plainBody = buildAllSubmissionsMailPlain($allSubmissions); $from = (string) ($smtp['from'] ?? $smtp['username'] ?? ''); $fromName = (string) ($smtp['fromName'] ?? '心理咨询系统'); try { return smtpSend($smtp, $recipients, $subject, $htmlBody, $plainBody, $from, $fromName); } catch (Throwable $e) { error_log('[mail] 发送失败: ' . $e->getMessage()); return false; } } function sortSubmissions(array $submissions): array { usort($submissions, static function ($a, $b) { return strcmp($a['createdAt'] ?? '', $b['createdAt'] ?? ''); }); return $submissions; } function escapeMailHtml(string $text): string { return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function normalizeSubmissionRow(array $record, int $index): array { $doctorName = trim((string) ($record['doctorName'] ?? '')); return [ 'index' => (string) $index, 'time' => formatSubmissionTime((string) ($record['createdAt'] ?? '')), 'nickname' => (string) ($record['nickname'] ?? ''), 'gender' => (string) ($record['gender'] ?? ''), 'contact' => (string) ($record['contact'] ?? ''), 'doctor' => $doctorName !== '' ? $doctorName : '未指定', 'description' => (string) ($record['description'] ?? ''), ]; } function buildAllSubmissionsMailHtml(array $submissions): string { $submissions = sortSubmissions($submissions); $count = count($submissions); $generatedAt = date('Y-m-d H:i:s'); if ($submissions === []) { return '

当前暂无提交记录。

'; } $rows = ''; foreach ($submissions as $i => $record) { $row = normalizeSubmissionRow($record, $i + 1); $bg = ($i % 2 === 0) ? '#ffffff' : '#f8fbf9'; $rows .= '' . '' . escapeMailHtml($row['index']) . '' . '' . escapeMailHtml($row['time']) . '' . '' . escapeMailHtml($row['nickname']) . '' . '' . escapeMailHtml($row['gender']) . '' . '' . escapeMailHtml($row['contact']) . '' . '' . escapeMailHtml($row['doctor']) . '' . '' . nl2br(escapeMailHtml($row['description'])) . '' . ''; } return <<

心理咨询预约记录汇总

共 {$count} 条记录 · 生成时间 {$generatedAt}

{$rows}
序号 提交时间 提交人 性别 联系电话 咨询师 需求简述

—— 心理咨询系统自动通知

HTML; } function buildAllSubmissionsMailPlain(array $submissions): string { $submissions = sortSubmissions($submissions); if ($submissions === []) { return "当前暂无提交记录。\n"; } $lines = [ '心理咨询预约记录汇总(共' . count($submissions) . '条)', str_repeat('=', 80), sprintf('%-4s %-19s %-8s %-4s %-13s %-10s %s', '序号', '提交时间', '提交人', '性别', '联系电话', '咨询师', '需求简述'), str_repeat('-', 80), ]; foreach ($submissions as $i => $record) { $row = normalizeSubmissionRow($record, $i + 1); $desc = preg_replace('/\s+/', ' ', $row['description']); $lines[] = sprintf( '%-4s %-19s %-8s %-4s %-13s %-10s %s', $row['index'], $row['time'], $row['nickname'], $row['gender'], $row['contact'], $row['doctor'], $desc ); } $lines[] = str_repeat('-', 80); $lines[] = '—— 心理咨询系统自动通知'; return implode("\n", $lines); } function smtpSend(array $smtp, array $recipients, string $subject, string $htmlBody, string $plainBody, string $from, string $fromName): bool { if ($recipients === []) { throw new RuntimeException('收件人为空'); } $host = (string) $smtp['host']; $port = (int) ($smtp['port'] ?? 465); $username = (string) ($smtp['username'] ?? ''); $password = (string) ($smtp['password'] ?? ''); $encryption = strtolower((string) ($smtp['encryption'] ?? 'ssl')); if ($username === '' || $password === '' || $from === '') { throw new RuntimeException('SMTP 账号或发件人未配置'); } $remote = ($encryption === 'ssl' ? 'ssl://' : '') . $host . ':' . $port; $context = stream_context_create([ 'ssl' => [ 'verify_peer' => true, 'verify_peer_name' => true, 'allow_self_signed' => false, ], ]); $socket = @stream_socket_client($remote, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context); if (!$socket) { throw new RuntimeException("无法连接 SMTP 服务器: {$errstr} ({$errno})"); } stream_set_timeout($socket, 30); try { smtpExpect($socket, [220]); smtpCommand($socket, 'EHLO localhost'); smtpExpect($socket, [250]); smtpCommand($socket, 'AUTH LOGIN'); smtpExpect($socket, [334]); smtpCommand($socket, base64_encode($username)); smtpExpect($socket, [334]); smtpCommand($socket, base64_encode($password)); smtpExpect($socket, [235]); smtpCommand($socket, 'MAIL FROM:<' . $from . '>'); smtpExpect($socket, [250]); foreach ($recipients as $to) { smtpCommand($socket, 'RCPT TO:<' . $to . '>'); smtpExpect($socket, [250, 251]); } smtpCommand($socket, 'DATA'); smtpExpect($socket, [354]); $message = buildMimeMessage($from, $fromName, $recipients, $subject, $htmlBody, $plainBody); fwrite($socket, $message . "\r\n.\r\n"); smtpExpect($socket, [250]); smtpCommand($socket, 'QUIT'); smtpExpect($socket, [221]); } finally { fclose($socket); } return true; } function buildMimeMessage(string $from, string $fromName, array $recipients, string $subject, string $htmlBody, string $plainBody): string { $encodedSubject = '=?UTF-8?B?' . base64_encode($subject) . '?='; $encodedFromName = '=?UTF-8?B?' . base64_encode($fromName) . '?='; $toHeader = implode(', ', array_map(static fn($email) => '<' . $email . '>', $recipients)); $boundary = 'b_' . md5(uniqid((string) mt_rand(), true)); $headers = [ 'From: ' . $encodedFromName . ' <' . $from . '>', 'To: ' . $toHeader, 'Subject: ' . $encodedSubject, 'MIME-Version: 1.0', 'Content-Type: multipart/alternative; boundary="' . $boundary . '"', 'Date: ' . date('r'), ]; $plainPart = chunk_split(base64_encode($plainBody), 76, "\r\n"); $htmlPart = chunk_split(base64_encode($htmlBody), 76, "\r\n"); $body = "--{$boundary}\r\n" . "Content-Type: text/plain; charset=UTF-8\r\n" . "Content-Transfer-Encoding: base64\r\n\r\n" . rtrim($plainPart) . "\r\n\r\n" . "--{$boundary}\r\n" . "Content-Type: text/html; charset=UTF-8\r\n" . "Content-Transfer-Encoding: base64\r\n\r\n" . rtrim($htmlPart) . "\r\n\r\n" . "--{$boundary}--"; return implode("\r\n", $headers) . "\r\n\r\n" . $body; } function smtpCommand($socket, string $command): void { fwrite($socket, $command . "\r\n"); } function smtpExpect($socket, array $validCodes): string { $response = ''; while (($line = fgets($socket, 515)) !== false) { $response .= $line; if (isset($line[3]) && $line[3] === ' ') { break; } } if ($response === '') { throw new RuntimeException('SMTP 无响应'); } $code = (int) substr($response, 0, 3); if (!in_array($code, $validCodes, true)) { throw new RuntimeException('SMTP 错误: ' . trim($response)); } return $response; } function finishResponseAndContinue(): void { if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); return; } if (ob_get_level() > 0) { ob_end_flush(); } flush(); if (function_exists('session_write_close')) { session_write_close(); } ignore_user_abort(true); }