= 4 && $line[3] === ' ') { break; } } return $data; } /** 发送命令并校验响应码 */ function fb_smtp_cmd($fp, $cmd, $codes, &$err, $hideLog = false) { if ($cmd !== null && $cmd !== '') { fwrite($fp, $cmd . "\r\n"); } $resp = fb_smtp_read($fp); $code = (int)substr(ltrim($resp), 0, 3); $want = is_array($codes) ? $codes : [(int)$codes]; if (!in_array($code, $want, true)) { $err = ($hideLog ? '(命令内容已隐藏) ' : '') . trim($resp); return false; } return true; } /** 邮件头编码(UTF-8 -> =?UTF-8?B?...?=) */ function fb_mail_header($s) { return '=?UTF-8?B?' . base64_encode((string)$s) . '?='; } /** * 发送一封 HTML 邮件 * @return array [bool ok, string err] */ function fb_smtp_send(array $s, $to, $subject, $html) { $err = ''; $host = (string)$s['host']; $port = (int)$s['port']; $secure = strtolower(isset($s['secure']) ? (string)$s['secure'] : ''); $timeout= (int)(isset($s['timeout']) ? $s['timeout'] : 15); $verify = !isset($s['verify_ssl']) || $s['verify_ssl']; if ($host === '' || $port <= 0) { return [false, 'SMTP 参数不完整']; } if (!filter_var($to, FILTER_VALIDATE_EMAIL)) { return [false, '收件地址非法']; } $sslOpt = $verify ? ['verify_peer' => true, 'verify_peer_name' => true] : ['verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true]; $ctx = stream_context_create(['ssl' => $sslOpt]); $remote = (($secure === 'ssl' || $secure === 'smtps') ? 'ssl://' : '') . $host . ':' . $port; $fp = @stream_socket_client($remote, $eno, $estr, $timeout, STREAM_CLIENT_CONNECT, $ctx); if (!$fp) { return [false, '连接失败: ' . $estr . ' (' . $eno . ')']; } stream_set_timeout($fp, $timeout); $hello = isset($_SERVER['HTTP_HOST']) ? (string)$_SERVER['HTTP_HOST'] : 'localhost'; $hello = preg_replace('/[^A-Za-z0-9\.\-]/', '', $hello); if ($hello === '') { $hello = 'localhost'; } if (!fb_smtp_cmd($fp, null, 220, $err)) { fclose($fp); return [false, '服务器问候失败: ' . $err]; } if (!fb_smtp_cmd($fp, 'EHLO ' . $hello, 250, $err)) { fclose($fp); return [false, 'EHLO 失败: ' . $err]; } if ($secure === 'tls' || $secure === 'starttls') { if (!fb_smtp_cmd($fp, 'STARTTLS', 220, $err)) { fclose($fp); return [false, 'STARTTLS 失败: ' . $err]; } if (!@stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { fclose($fp); return [false, 'TLS 握手失败']; } if (!fb_smtp_cmd($fp, 'EHLO ' . $hello, 250, $err)) { fclose($fp); return [false, 'EHLO(TLS) 失败: ' . $err]; } } $user = (string)(isset($s['user']) ? $s['user'] : ''); if ($user !== '') { if (!fb_smtp_cmd($fp, 'AUTH LOGIN', 334, $err)) { fclose($fp); return [false, 'AUTH LOGIN 不受支持: ' . $err]; } if (!fb_smtp_cmd($fp, base64_encode($user), 334, $err, true)) { fclose($fp); return [false, '用户名未被接受: ' . $err]; } if (!fb_smtp_cmd($fp, base64_encode((string)$s['pass']), 235, $err, true)) { fclose($fp); return [false, '认证失败(账号 / 密码 / 授权码): ' . $err]; } } $from = !empty($s['from']) ? (string)$s['from'] : $user; if (!filter_var($from, FILTER_VALIDATE_EMAIL)) { fclose($fp); return [false, '发件地址非法']; } if (!fb_smtp_cmd($fp, 'MAIL FROM:<' . $from . '>', 250, $err)) { fclose($fp); return [false, 'MAIL FROM 失败: ' . $err]; } if (!fb_smtp_cmd($fp, 'RCPT TO:<' . $to . '>', [250, 251], $err)) { fclose($fp); return [false, 'RCPT TO 失败: ' . $err]; } if (!fb_smtp_cmd($fp, 'DATA', 354, $err)) { fclose($fp); return [false, 'DATA 失败: ' . $err]; } $fromName = isset($s['from_name']) ? (string)$s['from_name'] : ''; $headers = []; $headers[] = 'Date: ' . date('r'); $headers[] = 'From: ' . ($fromName !== '' ? fb_mail_header($fromName) . ' ' : '') . '<' . $from . '>'; $headers[] = 'To: <' . $to . '>'; $headers[] = 'Subject: ' . fb_mail_header($subject); $headers[] = 'MIME-Version: 1.0'; $headers[] = 'Content-Type: text/html; charset=UTF-8'; $headers[] = 'Content-Transfer-Encoding: base64'; $headers[] = 'Message-ID: <' . bin2hex(random_bytes(12)) . '@' . $hello . '>'; $headers[] = 'X-Mailer: FeedLite'; // 正文用 base64,天然不含以 "." 开头的行,因此无需 dot-stuffing $body = chunk_split(base64_encode($html), 76, "\r\n"); $data = rtrim(implode("\r\n", $headers) . "\r\n\r\n" . $body, "\r\n"); if (fwrite($fp, $data . "\r\n.\r\n") === false) { fclose($fp); return [false, '写入邮件正文失败']; } if (!fb_smtp_cmd($fp, null, 250, $err)) { fclose($fp); return [false, '投递失败: ' . $err]; } @fb_smtp_cmd($fp, 'QUIT', 221, $e2); fclose($fp); return [true, '']; } /** 渲染通知邮件正文 */ function fb_notify_html($thread, $reply, $isAdmin) { $base = fb_base_url(); $url = $base . '/thread.php?id=' . (int)$thread['id']; $site = (string)fb_cfg('site_name'); $name = (string)$thread['author_name']; $title = (string)$thread['title']; $rname = $isAdmin ? '官方' : (string)$reply['author_name']; $rbody = nl2br(htmlspecialchars((string)$reply['body'], ENT_QUOTES, 'UTF-8')); $safeName = htmlspecialchars($name, ENT_QUOTES, 'UTF-8'); $safeTitle = htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); $safeRName = htmlspecialchars($rname, ENT_QUOTES, 'UTF-8'); return '
' . '

' . $safeName . ',你好:

' . '

你在「' . htmlspecialchars($site, ENT_QUOTES, 'UTF-8') . '」提交的问题 《' . $safeTitle . '》 有了新的回复:

' . '
' . '
' . $safeRName . ' 回复:
' . $rbody . '
' . '

查看并回复

' . '

若按钮无法点击,请复制链接到浏览器打开:
' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '

' . '
' . '

此邮件由系统自动发送,请勿直接回复。

' . '
'; } /** * 有人回复后,给发帖人发送通知邮件 * @return string 空串=未发送/已发送成功;非空=失败原因 */ function fb_notify_reply(PDO $pdo, array $thread, array $reply, $isAdmin) { $cfg = fb_cfg(); $nt = isset($cfg['notify']) ? (array)$cfg['notify'] : []; if (empty($nt['enabled'])) { return ''; } if (empty($nt['on_every_reply']) && !$isAdmin) { return ''; } $to = trim((string)$thread['author_email']); if ($to === '' || !filter_var($to, FILTER_VALIDATE_EMAIL)) { return ''; } // 自己回复自己:不发 if (!$isAdmin && !empty($reply['author_email']) && strcasecmp(trim((string)$reply['author_email']), $to) === 0) { return ''; } // 节流:同一帖同一收件人 N 秒内不重复发 $cd = (int)(isset($nt['cooldown_seconds']) ? $nt['cooldown_seconds'] : 0); if ($cd > 0) { $st = $pdo->prepare('SELECT created_at FROM notify_log WHERE thread_id=? AND to_email=? ORDER BY id DESC LIMIT 1'); $st->execute([(int)$thread['id'], $to]); $last = $st->fetchColumn(); if ($last !== false && (time() - (int)$last) < $cd) { return ''; } } $smtp = isset($cfg['smtp']) ? (array)$cfg['smtp'] : []; if (empty($smtp['enabled'])) { fb_notify_log($pdo, $thread, $reply, $to, 0, 'SMTP 未启用'); return 'SMTP 未启用'; } $subject = '【' . $cfg['site_name'] . '】有人回复了你的问题:' . fb_excerpt($thread['title'], 40); $html = fb_notify_html($thread, $reply, $isAdmin); list($ok, $err) = fb_smtp_send($smtp, $to, $subject, $html); fb_notify_log($pdo, $thread, $reply, $to, $ok ? 1 : 0, $ok ? null : $err); return $ok ? '' : $err; } /** 记录通知尝试 */ function fb_notify_log(PDO $pdo, array $thread, array $reply, $to, $ok, $err) { try { $st = $pdo->prepare('INSERT INTO notify_log(thread_id, reply_id, to_email, created_at, ok, err) VALUES(?,?,?,?,?,?)'); $st->execute([(int)$thread['id'], (int)$reply['id'], $to, time(), $ok ? 1 : 0, $err]); } catch (Exception $ex) { // 静默 } } /* ---------------- 关闭通知(判定"不是问题",写给提问人的说明邮件) ---------------- */ /** 关闭通知的邮件正文 */ function fb_notify_closed_html(array $thread, string $note) { $site = (string)fb_cfg('site_name'); $name = htmlspecialchars((string)$thread['author_name'], ENT_QUOTES, 'UTF-8'); $title = htmlspecialchars((string)$thread['title'], ENT_QUOTES, 'UTF-8'); $body = nl2br(htmlspecialchars((string)$thread['body'], ENT_QUOTES, 'UTF-8')); $noteH = nl2br(htmlspecialchars($note, ENT_QUOTES, 'UTF-8')); $mail = htmlspecialchars((string)fb_cfg('admin_email'), ENT_QUOTES, 'UTF-8'); return '
' . '

' . $name . ',你好:

' . '

感谢你反馈 《' . $title . '》。我们已经核实过,' . '该情况并不是应用的问题,因此这条反馈已被关闭(不会公开显示)。

' . '
' . '
官方说明:
' . $noteH . '
' . '

你当时反馈的内容:

' . '
' . $body . '
' . ($mail !== '' ? '

如果上面的说明没能解决你的疑问,可以直接回复本邮件,我们会继续跟进;' . '也可以发邮件到 ' . $mail . ' 补充说明。

' : '

如果上面的说明没能解决你的疑问,可以直接回复本邮件,我们会继续跟进。

') . '
' . '

此邮件由「' . htmlspecialchars($site, ENT_QUOTES, 'UTF-8') . '」自动发送。

' . '
'; } /** * 关闭问题(判定"不是问题")时,把说明邮件发给提问人 * @return array [bool 是否发送成功, string 失败原因(空串=成功或未启用)] */ function fb_notify_closed(PDO $pdo, array $thread, string $note) { $cfg = fb_cfg(); $sm = isset($cfg['smtp']) ? (array)$cfg['smtp'] : []; if (empty($sm['enabled'])) { // 仅记录,不算失败 try { $st = $pdo->prepare('INSERT INTO notify_log(thread_id, reply_id, to_email, created_at, ok, err) VALUES(?,?,?,?,?,?)'); $st->execute([(int)$thread['id'], 0, (string)$thread['author_email'], time(), 0, 'SMTP 未启用,关闭通知未发送']); } catch (Exception $ex) { /* 静默 */ } return [false, 'SMTP 未启用,邮件未发送(已在配置里关闭)']; } $to = (string)$thread['author_email']; if (!filter_var($to, FILTER_VALIDATE_EMAIL)) { return [false, '提问人邮箱不可用,未发送']; } $tpl = (string)fb_cfg('moderation.close_mail_subject', '关于你反馈的问题《%s》'); $subject = sprintf($tpl, (string)$thread['title']); $html = fb_notify_closed_html($thread, $note); list($ok, $err) = fb_smtp_send($sm, $to, $subject, $html); try { $st = $pdo->prepare('INSERT INTO notify_log(thread_id, reply_id, to_email, created_at, ok, err) VALUES(?,?,?,?,?,?)'); $st->execute([(int)$thread['id'], 0, $to, time(), $ok ? 1 : 0, $ok ? null : $err]); } catch (Exception $ex) { /* 静默 */ } return [$ok, $ok ? '' : $err]; }