Skip to content

feat(server): EO Pages 支持自定义 SMTP#980

Merged
imaegoo merged 7 commits into
twikoojs:mainfrom
jeoor:eo-pages-go-smtp
Jun 5, 2026
Merged

feat(server): EO Pages 支持自定义 SMTP#980
imaegoo merged 7 commits into
twikoojs:mainfrom
jeoor:eo-pages-go-smtp

Conversation

@jeoor

@jeoor jeoor commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

原因

Fixes #979

EdgeOne Pages 的 Node Function 运行时不支持 TCP socket,无法通过 Nodemailer 直接连接自定义 SMTP 服务器,导致 EO Pages 目前只能使用 SendGrid 和 MailChannels 发送邮件。

由于 EO Pages 的 Blob SDK 仍由 Node Function 使用,本 PR 保留 Node 作为 Twikoo 主后端,仅新增 Go Cloud Function 处理 SMTP TCP 连接。

Summary

  • 新增 EO Pages Go Cloud Function /smtp,负责自定义 SMTP 的 verifysend
  • EO Pages Node Function 在配置 SMTP_HOST 时通过 HTTP 调用同项目 /smtp
  • 保留 SendGrid 和 MailChannels 的现有 HTTP API 发送逻辑
  • URL 模式客户端请求携带实际 envId,用于跨域博客场景自动发现同项目 /smtp
  • 使用 TWIKOO_SMTP_BRIDGE_TOKEN 保护 /smtp,避免公开接口被滥用
  • 补充 EO Pages 自定义 SMTP 配置文档和构建检查

使用说明

使用自定义 SMTP 时需要配置:

  • TWIKOO_SMTP_BRIDGE_TOKEN
  • SMTP_HOST
  • SMTP_PORT
  • SMTP_SECURE
  • SMTP_USER
  • SMTP_PASS
  • SENDER_EMAIL

SMTP_SERVICE=SendGridSMTP_SERVICE=MailChannels 继续走原有 HTTP API,不经过 Go SMTP Bridge。

Verification

  • node --check src/server/eo-pages/cloud-functions/index.js
  • pnpm exec eslint src/server/eo-pages/cloud-functions/index.js src/client/utils/api.js
  • GO111MODULE=off go test in src/server/eo-pages/cloud-functions
  • git diff --check origin/main
  • EdgeOne Pages 线上邮件测试返回成功,并实际收到测试邮件
EO Pages 自定义 SMTP 邮件测试成功 收到 Twikoo 邮件通知测试邮件

Summary by Sourcery

添加一个 EO Pages SMTP 桥接组件,通过 Go Cloud Function 启用自定义 SMTP,同时保留现有的 SendGrid/MailChannels 支持,并将其集成到 Node 运行时的邮件流程中。

新功能:

  • 引入基于 Go 的 EO Pages /smtp Cloud Function,用于处理自定义 SMTP 验证以及通过 TCP 发送邮件。
  • 添加 Node 端 SMTP 桥接组件,通过受保护的 token 和基于 envId 的自动发现机制,发现并调用项目本地的 /smtp 端点。
  • 允许 EO Pages 部署在现有 SendGrid 和 MailChannels 集成的基础上,配置自定义 SMTP 连接器。

增强:

  • 在评论提交和邮件测试流程中传播邮件上下文,以便 SMTP 桥的发现和认证可以按请求执行。
  • 扩展 EO Pages 构建脚本,要求并文档化项目结构中新增加的 smtp.go 函数。

文档:

  • 编写 EO Pages 自定义 SMTP 配置文档,包括所需环境变量、桥接 token 的使用方式以及与现有 SendGrid/MailChannels 选项的交互方式。

测试:

  • 为新的 SMTP 桥接 Cloud Function 添加 Go 测试,用于验证连接、认证和探针处理。
Original summary in English

Summary by Sourcery

Add an EO Pages SMTP bridge that enables custom SMTP via a Go Cloud Function while preserving existing SendGrid/MailChannels support and integrating it with the Node runtime’s mail flow.

New Features:

  • Introduce a Go-based EO Pages /smtp Cloud Function to handle custom SMTP verification and email sending over TCP.
  • Add a Node-side SMTP bridge that discovers and calls the project-local /smtp endpoint using a protected token and envId-based auto-discovery.
  • Allow EO Pages deployments to configure custom SMTP connectors alongside existing SendGrid and MailChannels integrations.

Enhancements:

  • Propagate mail context through comment submission and email test flows so SMTP bridge discovery and authentication can run per-request.
  • Extend the EO Pages build script to require and document the new smtp.go function in the project structure.

Documentation:

  • Document EO Pages custom SMTP setup, including required environment variables, bridge token usage, and interaction with existing SendGrid/MailChannels options.

Tests:

  • Add Go tests for the new SMTP bridge Cloud Function to validate connection, auth, and probe handling.

@sourcery-ai

sourcery-ai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

审阅者指南

为 EdgeOne Pages 新增一个基于 Go 的 SMTP bridge,并将其与现有 Node 运行时集成,使 EO Pages 在保留现有 SendGrid/MailChannels HTTP 流程的同时,可以通过自定义 SMTP 发送邮件。

EO Pages 通过 Go bridge 发送自定义 SMTP 邮件的时序图

sequenceDiagram
  actor User
  participant Client as Browser_client
  participant Node as Node_function_index_js
  participant Go as Go_smtp_Handler
  participant SMTP as SMTP_server

  User->>Client: trigger emailTest / commentSubmit
  Client->>Node: call(event, data, envId)
  Node->>Node: createMailBridgeContext(req)
  Node->>Node: withMailBridgeContext(mailContext, emailTest)
  Node->>Node: nodemailer.createTransport(mailConfig)
  Node->>Node: sendMail({from,to,subject,html})
  Note over Node: mailConfig.host set (custom SMTP)
  Node->>Go: requestSmtpBridge(send, mailConfig, mail)
  Go->>Go: Handler(w, r)
  Go->>Go: authorizeSMTPBridge(r)
  Go->>Go: sendSMTP(req)
  Go->>SMTP: openSMTPClient(req)
  SMTP-->>Go: SMTP response
  Go-->>Node: JSON { ok: true }
  Node-->>Client: emailTest / sendNotice success
  Client-->>User: show success result
Loading

文件级变更

Change Details Files
在 Node 中为 EO Pages 引入 SMTP bridge,包括围绕独立 Go Cloud Function 的发现、验证以及请求打通。
  • 新增基于 AsyncLocalStorage 的 mail bridge 上下文以及辅助工具,用于根据 envId、origin 和 Host 头发现 /smtp bridge 的 URL。
  • 实现基于 HMAC 的探测验证和抗计时攻击比较,以安全地自动发现并缓存可用的 SMTP bridge URL。
  • 新增 requestSmtpBridge 辅助函数,用带有认证 token 和结构化负载的请求调用 Go 的 /smtp 端点,并通过 createMailBridgeContext 将其接入请求生命周期。
src/server/eo-pages/cloud-functions/index.js
扩展 Nodemailer shim,通过 bridge 支持自定义 SMTP,同时保留现有的 SendGrid/MailChannels HTTP 实现。
  • 重构 Nodemailer shim,用于规范化邮件服务名称并校验认证凭据。
  • 当配置了 SMTP_HOST 时,将 verify() 和 sendMail() 调用路由到 Go SMTP bridge,同时对 SendGrid/MailChannels 继续使用它们的 HTTP API。
  • 改进错误信息,清晰描述 EdgeOne Pages 上支持的 SMTP_SERVICE 与 SMTP_HOST 组合。
src/server/eo-pages/cloud-functions/index.js
在异步流程中传播 mail bridge 上下文,使通知/邮件测试路径能够使用 SMTP bridge。
  • 为每个请求向 postSubmit 传入 mail bridge 上下文,并用 withMailBridgeContext 包裹 sendNotice/emailTest 调用。
  • 在 onRequest 中为合成的 req 对象增加 SMTP bridge 发现所需的 env 和 origin 字段。
  • 确保 URL 模式的客户端请求包含 envId,以便服务器在跨域场景下推断 bridge URL。
src/server/eo-pages/cloud-functions/index.js
src/client/utils/api.js
新增实现 SMTP bridge 的 Go Cloud Function,包括 token 认证、探测签名以及 SMTP 客户端逻辑。
  • 实现一个 HTTP handler,使用 TWIKOO_SMTP_BRIDGE_TOKEN 校验 Authorization,并分发 verify/send/probe 操作。
  • 实现健壮的 SMTP 客户端逻辑,支持 TLS/STARTTLS、PLAIN/LOGIN 认证、超时控制以及显式的 MAIL/RCPT/DATA 处理。
  • 为 HTML 邮件构建符合 RFC 的 MIME 消息,使用 quoted-printable 编码并进行安全的头部格式化。
  • 添加基于 HMAC 的签名探测响应,以支持 Node 端的安全自动 bridge 发现。
src/server/eo-pages/cloud-functions/smtp.go
更新 EO Pages 文档和构建检查,以支持新的 SMTP bridge。
  • 在 README 中记录自定义 SMTP 的配置方式,包括 TWIKOO_SMTP_BRIDGE_TOKEN、SMTP_* 变量以及其与 SMTP_SERVICE 的交互。
  • 扩展构建时的文件存在性检查和项目结构输出,将新的 smtp.go bridge 纳入其中。
  • 视需要为服务器端构建产物添加 EO Pages 专用的 .gitignore。
src/server/eo-pages/README.md
src/server/eo-pages/build.cjs
src/server/eo-pages/.gitignore

与关联 Issue 的对照评估

Issue Objective Addressed Explanation
#979 使部署在 EdgeOne(EO Pages)上的实例能够使用自定义 SMTP 服务器发送邮件通知,而不再局限于 SendGrid 和 MailChannels。
#979 更新 EO Pages 后端逻辑和配置,使邮件验证/测试以及通知在新的自定义 SMTP 支持下能够正常工作,并提供合适的错误信息。
#979 文档化如何在 EO Pages 上配置自定义 SMTP(包括新增环境变量和部署时检查),以便用户真正能启用并使用该能力。

可能关联的 Issue


技巧与命令

与 Sourcery 交互

  • 触发新审阅: 在 pull request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审阅评论。
  • 从审阅评论生成 GitHub issue: 在回复某条审阅评论时请求 Sourcery 从该评论创建 issue。你也可以直接回复该评论并写 @sourcery-ai issue 来从中创建 issue。
  • 生成 pull request 标题: 在 pull request 标题任意位置写上 @sourcery-ai 即可随时生成标题。也可以在 pull request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文任意位置写上 @sourcery-ai summary,即可在对应位置生成 PR 摘要。也可以在 pull request 中评论 @sourcery-ai summary 来(重新)生成摘要。
  • 生成审阅者指南: 在 pull request 中评论 @sourcery-ai guide,可随时(重新)生成审阅者指南。
  • 一次性解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve,即可标记解决所有 Sourcery 评论。如果你已经处理了所有评论且不希望再看到它们,这会很有用。
  • 一次性撤销所有 Sourcery 审阅: 在 pull request 中评论 @sourcery-ai dismiss,即可撤销所有现有的 Sourcery 审阅。尤其适用于希望在新一轮审阅前“清空历史”——别忘了之后再评论 @sourcery-ai review 来触发新的审阅!

自定义你的体验

访问你的 dashboard 以:

  • 启用或禁用审阅功能,例如 Sourcery 自动生成的 pull request 摘要、审阅者指南等。
  • 更改审阅语言。
  • 添加、删除或编辑自定义审阅指令。
  • 调整其他审阅设置。

获取帮助

Original review guide in English

Reviewer's Guide

Adds a Go-based SMTP bridge for EdgeOne Pages and integrates it with the existing Node runtime so EO Pages can send mail via custom SMTP while preserving existing SendGrid/MailChannels HTTP flows.

Sequence diagram for EO Pages custom SMTP mail send via Go bridge

sequenceDiagram
  actor User
  participant Client as Browser_client
  participant Node as Node_function_index_js
  participant Go as Go_smtp_Handler
  participant SMTP as SMTP_server

  User->>Client: trigger emailTest / commentSubmit
  Client->>Node: call(event, data, envId)
  Node->>Node: createMailBridgeContext(req)
  Node->>Node: withMailBridgeContext(mailContext, emailTest)
  Node->>Node: nodemailer.createTransport(mailConfig)
  Node->>Node: sendMail({from,to,subject,html})
  Note over Node: mailConfig.host set (custom SMTP)
  Node->>Go: requestSmtpBridge(send, mailConfig, mail)
  Go->>Go: Handler(w, r)
  Go->>Go: authorizeSMTPBridge(r)
  Go->>Go: sendSMTP(req)
  Go->>SMTP: openSMTPClient(req)
  SMTP-->>Go: SMTP response
  Go-->>Node: JSON { ok: true }
  Node-->>Client: emailTest / sendNotice success
  Client-->>User: show success result
Loading

File-Level Changes

Change Details Files
Introduce an EO Pages SMTP bridge in Node, including discovery, verification, and request plumbing around a separate Go Cloud Function.
  • Add AsyncLocalStorage-based mail bridge context and helper utilities for discovering the /smtp bridge URL from envId, origin, and Host headers.
  • Implement HMAC-based probe verification and timing-safe comparison to securely auto-discover and cache a working SMTP bridge URL.
  • Add requestSmtpBridge helper to call the Go /smtp endpoint with auth token and structured payload, and wire it into the request lifecycle via createMailBridgeContext.
src/server/eo-pages/cloud-functions/index.js
Extend Nodemailer shim to support custom SMTP via the bridge while keeping existing SendGrid/MailChannels HTTP implementations.
  • Refactor Nodemailer shim to normalize mail service names and validate auth credentials.
  • Route verify() and sendMail() calls to the Go SMTP bridge when SMTP_HOST is configured, while continuing to use SendGrid/MailChannels HTTP APIs for those services.
  • Improve error messages to clearly describe supported combinations of SMTP_SERVICE and SMTP_HOST on EdgeOne Pages.
src/server/eo-pages/cloud-functions/index.js
Propagate mail bridge context through async flows so notification/email test paths can use the SMTP bridge.
  • Pass a per-request mail bridge context into postSubmit and wrap sendNotice/emailTest calls with withMailBridgeContext.
  • Augment the synthetic req object in onRequest with env and origin fields needed for SMTP bridge discovery.
  • Ensure URL-mode client requests include envId so the server can infer the bridge URL in cross-origin setups.
src/server/eo-pages/cloud-functions/index.js
src/client/utils/api.js
Add a Go Cloud Function implementing the SMTP bridge, including token auth, probe signing, and SMTP client logic.
  • Implement an HTTP handler that validates Authorization using TWIKOO_SMTP_BRIDGE_TOKEN and dispatches verify/send/probe actions.
  • Implement robust SMTP client logic with TLS/STARTTLS, PLAIN/LOGIN auth, timeouts, and explicit MAIL/RCPT/DATA handling.
  • Build RFC-compliant MIME messages for HTML emails using quoted-printable encoding and safe header formatting.
  • Add HMAC-based signed probe responses to support secure automatic bridge discovery from the Node side.
src/server/eo-pages/cloud-functions/smtp.go
Update EO Pages documentation and build checks for the new SMTP bridge.
  • Document custom SMTP setup, including TWIKOO_SMTP_BRIDGE_TOKEN, SMTP_* vars, and interaction with SMTP_SERVICE in README.
  • Extend build-time file presence checks and project structure output to include the new smtp.go bridge.
  • Add EO Pages-specific .gitignore for server-side artifacts if needed.
src/server/eo-pages/README.md
src/server/eo-pages/build.cjs
src/server/eo-pages/.gitignore

Assessment against linked issues

Issue Objective Addressed Explanation
#979 Enable deployments on EdgeOne (EO Pages) to use a custom SMTP server for email notifications instead of being limited to SendGrid and MailChannels.
#979 Update the EO Pages backend logic and configuration so that email verification/tests and notifications work correctly with the new custom SMTP support, including appropriate error messages.
#979 Document how to configure custom SMTP on EO Pages (including any new environment variables and deployment checks) so users can actually enable and use it.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了两个问题,并给出了一些整体性的反馈:

  • openSMTPClient 中,你创建了一个 context.WithTimeout,但在拨号时从未使用该 context(非 TLS 路径使用 DialContext,但 TLS 路径忽略了 ctx,只依赖 Dialer.Timeout);请要么通过 tls.Dialer/DialContext 传递该 context,要么移除未使用的 context,以避免混淆。
  • 当前 SMTP bridge 的错误路径会直接将原始的 smtp/网络错误消息返回到 JSON 的 message 字段中,这可能会向客户端泄露内部服务器细节;请考虑在将错误返回给调用方之前对这些错误进行包装或清洗。
面向 AI Agent 的提示
请根据以下代码审查意见进行修改:

## 总体评论
-`openSMTPClient` 中,你创建了一个 `context.WithTimeout`,但在拨号时从未使用该 context(非 TLS 路径使用 `DialContext`,但 TLS 路径忽略了 `ctx`,只依赖 `Dialer.Timeout`);请要么通过 `tls.Dialer`/`DialContext` 传递该 context,要么移除未使用的 context,以避免混淆。
- 当前 SMTP bridge 的错误路径会直接将原始的 `smtp`/网络错误消息返回到 JSON 的 `message` 字段中,这可能会向客户端泄露内部服务器细节;请考虑在将错误返回给调用方之前对这些错误进行包装或清洗。

## 单独评论

### 评论 1
<location path="src/server/eo-pages/cloud-functions/smtp.go" line_range="287-291" />
<code_context>
+	return "LOGIN", nil, nil
+}
+
+func (auth loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
+	if !more {
+		return nil, nil
+	}
+	challenge := strings.ToLower(strings.TrimSpace(string(fromServer)))
+	if strings.Contains(challenge, "user") {
+		return []byte(auth.username), nil
</code_context>
<issue_to_address>
**issue (bug_risk):** SMTP LOGIN 质询处理假设服务器返回明文提示,但服务器通常会发送 base64 编码的质询。

这里将 `fromServer` 作为明文解释,并与 "user"/"pass" 进行匹配,但实际服务器会发送 base64 编码的提示(例如 `VXNlcm5hbWU6``UGFzc3dvcmQ6`)。这会导致 LOGIN 在实际使用中失败。请在检查质询之前先对其进行解码,或者采用更完整的 LOGIN 实现,以确保已声明支持的机制真正能正常工作。
</issue_to_address>

### 评论 2
<location path="src/server/eo-pages/cloud-functions/index.js" line_range="161-170" />
<code_context>
+  }
+}
+
+async function verifySmtpBridgeUrl (url, token) {
+  const host = getSmtpBridgeCandidateHost(url)
+  if (!host) return false
+
+  const nonce = randomBytes(16).toString('hex')
+  let response
+  try {
+    response = await fetch(url, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ action: 'probe', nonce, bridgeHost: host })
+    })
+  } catch (e) {
+    return false
+  }
+
+  let result
+  try {
+    result = await response.json()
+  } catch (e) {
+    return false
+  }
+
+  const expected = signSmtpBridgeProbe(token, nonce, host)
+  return response.ok &&
+    result.ok &&
+    result.bridgeHost === host &&
+    timingSafeEqualHex(result.signature, expected)
+}
</code_context>
<issue_to_address>
**suggestion (performance):** SMTP bridge URL 验证缺少超时控制,当候选主机响应缓慢或无响应时,存在长时间挂起的风险。

当前用于探测的 `fetch` 调用没有超时或中止信号,因此当候选主机响应慢、被黑洞路由或配置错误时,`getSmtpBridgeUrl` 可能会长时间阻塞,从而影响邮件测试和提交后的通知。请为每次探测添加超时(例如使用 `AbortController`),以便在候选主机未及时响应时快速失败。

建议实现如下:

```javascript
const SMTP_BRIDGE_PROBE_TIMEOUT_MS = 5000

async function verifySmtpBridgeUrl (url, token) {
  const host = getSmtpBridgeCandidateHost(url)
  if (!host) return false

  const nonce = randomBytes(16).toString('hex')
  let response

  const controller = new AbortController()
  const timeoutId = setTimeout(() => {
    controller.abort()
  }, SMTP_BRIDGE_PROBE_TIMEOUT_MS)

  try {
    response = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ action: 'probe', nonce, bridgeHost: host }),
      signal: controller.signal
    })
  } catch (e) {
    return false
  } finally {
    clearTimeout(timeoutId)
  }

  let result
  try {
    result = await response.json()
  } catch (e) {
    return false
  }

  const expected = signSmtpBridgeProbe(token, nonce, host)
  return response.ok &&
    result.ok &&
    result.bridgeHost === host &&
    timingSafeEqualHex(result.signature, expected)
}

```

1. 如果该文件已经定义了共享的超时/配置常量,请将 `SMTP_BRIDGE_PROBE_TIMEOUT_MS` 放在这些常量旁边,或者重用已有的 HTTP 超时常量以保持一致。
2. 确保运行环境提供 `AbortController` 以及支持 `signal` 选项的 `fetch` 实现;如果没有,请在文件顶部添加相应的 polyfill/导入(例如来自 `node-fetch` 或类似库)。

Sourcery 对开源项目免费——如果你觉得这些评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续评审。
Original comment in English

Hey - I've found 2 issues, and left some high level feedback:

  • In openSMTPClient you create a context.WithTimeout but never use the context for dialing (the non-TLS path uses DialContext but the TLS path ignores ctx and relies only on Dialer.Timeout); either wire the context through tls.Dialer/DialContext or remove the unused context to avoid confusion.
  • The SMTP bridge error path currently returns raw smtp/network error messages directly in the JSON message field, which may leak internal server details to clients; consider wrapping or sanitizing these errors before sending them back to the caller.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `openSMTPClient` you create a `context.WithTimeout` but never use the context for dialing (the non-TLS path uses `DialContext` but the TLS path ignores `ctx` and relies only on `Dialer.Timeout`); either wire the context through `tls.Dialer`/`DialContext` or remove the unused context to avoid confusion.
- The SMTP bridge error path currently returns raw `smtp`/network error messages directly in the JSON `message` field, which may leak internal server details to clients; consider wrapping or sanitizing these errors before sending them back to the caller.

## Individual Comments

### Comment 1
<location path="src/server/eo-pages/cloud-functions/smtp.go" line_range="287-291" />
<code_context>
+	return "LOGIN", nil, nil
+}
+
+func (auth loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
+	if !more {
+		return nil, nil
+	}
+	challenge := strings.ToLower(strings.TrimSpace(string(fromServer)))
+	if strings.Contains(challenge, "user") {
+		return []byte(auth.username), nil
</code_context>
<issue_to_address>
**issue (bug_risk):** SMTP LOGIN challenge handling assumes plain-text prompts, but servers typically send base64-encoded challenges.

Here `fromServer` is interpreted as plain text and matched against "user"/"pass", but real servers send base64-encoded prompts (e.g., `VXNlcm5hbWU6`, `UGFzc3dvcmQ6`). This will cause LOGIN to fail in practice. Please decode the challenge before inspecting it or adopt a more complete LOGIN implementation so the advertised mechanism actually works.
</issue_to_address>

### Comment 2
<location path="src/server/eo-pages/cloud-functions/index.js" line_range="161-170" />
<code_context>
+  }
+}
+
+async function verifySmtpBridgeUrl (url, token) {
+  const host = getSmtpBridgeCandidateHost(url)
+  if (!host) return false
+
+  const nonce = randomBytes(16).toString('hex')
+  let response
+  try {
+    response = await fetch(url, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ action: 'probe', nonce, bridgeHost: host })
+    })
+  } catch (e) {
+    return false
+  }
+
+  let result
+  try {
+    result = await response.json()
+  } catch (e) {
+    return false
+  }
+
+  const expected = signSmtpBridgeProbe(token, nonce, host)
+  return response.ok &&
+    result.ok &&
+    result.bridgeHost === host &&
+    timingSafeEqualHex(result.signature, expected)
+}
</code_context>
<issue_to_address>
**suggestion (performance):** SMTP bridge URL verification lacks a timeout, risking long hangs when a candidate host is slow or unresponsive.

The `fetch` probe runs without a timeout or abort signal, so `getSmtpBridgeUrl` can block for a long time if a candidate is slow, blackholed, or misconfigured, impacting email tests and post-submit notifications. Please add a per-probe timeout (for example using an `AbortController`) so discovery fails fast when a candidate doesn’t respond promptly.

Suggested implementation:

```javascript
const SMTP_BRIDGE_PROBE_TIMEOUT_MS = 5000

async function verifySmtpBridgeUrl (url, token) {
  const host = getSmtpBridgeCandidateHost(url)
  if (!host) return false

  const nonce = randomBytes(16).toString('hex')
  let response

  const controller = new AbortController()
  const timeoutId = setTimeout(() => {
    controller.abort()
  }, SMTP_BRIDGE_PROBE_TIMEOUT_MS)

  try {
    response = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ action: 'probe', nonce, bridgeHost: host }),
      signal: controller.signal
    })
  } catch (e) {
    return false
  } finally {
    clearTimeout(timeoutId)
  }

  let result
  try {
    result = await response.json()
  } catch (e) {
    return false
  }

  const expected = signSmtpBridgeProbe(token, nonce, host)
  return response.ok &&
    result.ok &&
    result.bridgeHost === host &&
    timingSafeEqualHex(result.signature, expected)
}

```

1. If this file already defines shared timeout/configuration constants, move `SMTP_BRIDGE_PROBE_TIMEOUT_MS` next to them or reuse an existing HTTP timeout constant to stay consistent.
2. Ensure that the runtime environment provides `AbortController` and a `fetch` implementation that supports the `signal` option; if not, add the appropriate polyfill/import at the top of the file (e.g. from `node-fetch` or a similar library).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/server/eo-pages/cloud-functions/smtp.go Outdated
Comment thread src/server/eo-pages/cloud-functions/index.js
@jeoor

jeoor commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

已根据 review 反馈在 1fc6650 中补充修正:

  • openSMTPClient 的 TLS 路径改用 tls.Dialer.DialContext,让统一的 timeout context 在 TLS/非 TLS 拨号中都生效。
  • LOGIN auth 现在会先尝试解码 base64 challenge,并保留 step-based fallback,兼容常见 SMTP 服务器返回的 Username: / Password: challenge。
  • /smtp probe 自动发现增加 AbortController 超时控制,避免候选地址无响应时长时间阻塞。
  • SMTP bridge 返回给调用方的网络/SMTP 错误已做脱敏,避免直接暴露底层连接或服务器细节。

已重新验证:

  • node --check src/server/eo-pages/cloud-functions/index.js
  • pnpm exec eslint src/server/eo-pages/cloud-functions/index.js src/client/utils/api.js
  • GO111MODULE=off go test in src/server/eo-pages/cloud-functions
  • git diff --check

@imaegoo imaegoo merged commit 54d869a into twikoojs:main Jun 5, 2026
2 checks passed
@jeoor jeoor deleted the eo-pages-go-smtp branch June 27, 2026 08:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

部署在 edgeone 的 twikoo 的邮箱提醒仅支持 SendGrid 和 MailChannels 邮件服务吗?

2 participants