[add] Lark Email API based on Node-Mailer 8#57
Conversation
[optimize] update Upstream packages
📝 WalkthroughWalkthrough扩展环境变量以支持 SMTP(SMTP_HOST、SMTP_PORT、SMTP_USER、SMTP_PASSWORD),并新增一个 Next.js Pages API 路由 Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant NextAPI as Next.js API (with safeAPI & verifyJWT)
participant Handler
participant Transporter as Nodemailer Transporter
participant SMTP as SMTP Server
Client->>NextAPI: POST /api/Lark/mail/:address/message (raw body)
NextAPI->>NextAPI: verifyJWT / safeAPI 中间件
NextAPI->>Handler: forward request
Handler->>Transporter: transporter.sendMail(mailOptions with from=SMTP_USER)
Transporter->>SMTP: SMTP transaction (AUTH, MAIL FROM, RCPT TO, DATA)
SMTP-->>Transporter: 2xx success / error
Transporter-->>Handler: send result
Handler-->>Client: 200 OK / error response
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Documentation Updates 2 document(s) were updated by changes in this PR: Backend Framework and API DesignView Changes@@ -128,11 +128,12 @@
#### Lark Email API
-The system includes an email sending API at `/api/Lark/mail/[address]/message` that uses nodemailer (v8) to send emails through an SMTP server. This endpoint is built using Next.js API routes with `next-ssr-middleware` and accepts POST requests with nodemailer `Mail.Options` in the request body.
+The system includes an email sending API at `/api/Lark/mail/[address]/message` that uses nodemailer 8.0.1 to send emails through an SMTP server. This endpoint is built using Next.js API routes with `next-ssr-middleware` and accepts POST requests with nodemailer `Mail.Options` in the request body.
The email API:
- Uses `createTransport` to configure an SMTP connection with credentials from environment variables.
+- Secured with JWT verification middleware (`verifyJWT`).
- Automatically sets the "from" address to the configured `SMTP_USER`.
- Sends emails through the configured SMTP server using `sendMail`.
- Returns the result of the send operation to the caller.
@@ -148,13 +149,13 @@
auth: { user: SMTP_USER, pass: SMTP_PASSWORD },
});
-router.post('/bot/message', async context => {
+router.post('/bot/message', safeAPI, verifyJWT, async context => {
const input = Reflect.get(context.request, 'body') as Mail.Options;
context.body = await transporter.sendMail({ ...input, from: SMTP_USER });
});
```
-The API configuration disables Next.js's built-in body parser (`bodyParser: false`) to allow `next-ssr-middleware` to handle request parsing with Koa-compatible middleware.
+The endpoint integrates with Next.js API routes using `next-ssr-middleware` utilities (`createKoaRouter` and `withKoaRouter`). The API configuration disables Next.js's built-in body parser (`bodyParser: false`) to allow `next-ssr-middleware` to handle request parsing with Koa-compatible middleware.
### Environment and Configuration
CI/CD and Deployment AutomationView Changes@@ -1,25 +1,54 @@
## Continuous Integration and Deployment Automation
### GitHub Actions CI/CD Pipeline
-The project uses GitHub Actions workflows for continuous integration and deployment. Workflow files are located in the `.github/workflows` directory and include jobs for building, testing, and deploying the application. For example, the `main.yml` workflow is triggered on every push to any branch. It checks out the code, runs build steps, and, if configured, deploys to Vercel. Environment secrets such as `VERCEL_TOKEN`, `VERCEL_ORG_ID`, and `VERCEL_PROJECT_ID` are required for deployment steps. The workflow uses the `amondnet/vercel-action@v25` action to deploy to Vercel, and production deployments are triggered when the branch is `main` [(source)](https://github.com/Open-Source-Bazaar/Open-Source-Bazaar.github.io/blob/f5df98635f7a8bdab44cded44633ecbc3145a73f/.github/workflows/main.yml).
+The project uses GitHub Actions workflows for continuous integration and deployment. Workflow files are located in the `.github/workflows` directory and include jobs for building, testing, and deploying the application. For example, the `main.yml` workflow is triggered on every push to any branch. It checks out the code, runs build steps, and, if configured, deploys to Vercel. Environment secrets such as `VERCEL_TOKEN`, `VERCEL_ORG_ID`, and `VERCEL_PROJECT_ID` are required for deployment steps. The workflow uses the Vercel CLI to deploy, and production deployments are triggered when the branch is `main` [(source)](https://github.com/Open-Source-Bazaar/Open-Source-Bazaar.github.io/blob/f5df98635f7a8bdab44cded44633ecbc3145a73f/.github/workflows/main.yml).
Other repositories may include additional workflows such as `deploy-production.yml`, `init-template.yml`, and `publish-type.yml` for specialized deployment and initialization tasks [(source)](https://github.com/Open-Source-Bazaar/ActivityHub-service/pull/8).
### Vercel Deployment
-Vercel deployment is fully automated via GitHub Actions. The deployment step uses the `amondnet/vercel-action@v25` action, passing in the necessary secrets for authentication. The deployment is triggered on every push, but the `--prod` flag is only used when deploying from the `main` branch. After deployment, the workflow outputs a preview URL, which is included in notifications [(source)](https://github.com/Open-Source-Bazaar/Open-Source-Bazaar.github.io/blob/f5df98635f7a8bdab44cded44633ecbc3145a73f/.github/workflows/main.yml).
+Vercel deployment is fully automated via GitHub Actions using the Vercel CLI. The deployment step first sets up Node.js 24, then installs the Vercel CLI globally with `npm install vercel -g`. The deployment script uses `set -euo pipefail` for error handling and safety. For production deployments (from the `main` branch), it runs `vercel -t "$VERCEL_TOKEN" --prod`, while preview deployments use `vercel -t "$VERCEL_TOKEN"`. After deployment, the script parses the deployment URL from the output using `grep` and regex, then uses the `vercel inspect` command with JSON output to extract deployment details. The preview URL is set as a GitHub Actions output for use in subsequent steps, such as notifications [(source)](https://github.com/Open-Source-Bazaar/Open-Source-Bazaar.github.io/blob/f5df98635f7a8bdab44cded44633ecbc3145a73f/.github/workflows/main.yml).
Example deployment step:
```yaml
+- uses: actions/setup-node@v6
+ if: ${{ env.VERCEL_TOKEN && env.VERCEL_ORG_ID && env.VERCEL_PROJECT_ID }}
+ with:
+ node-version: 24
+
- name: Deploy to Vercel
id: vercel-deployment
- uses: amondnet/vercel-action@v25
- with:
- vercel-token: ${{ secrets.VERCEL_TOKEN }}
- github-token: ${{ secrets.GITHUB_TOKEN }}
- vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
- vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
- working-directory: ./
- vercel-args: ${{ github.ref == 'refs/heads/main' && ' --prod' || '' }}
+ if: ${{ env.VERCEL_TOKEN && env.VERCEL_ORG_ID && env.VERCEL_PROJECT_ID }}
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ npm install vercel -g
+
+ if [[ "$GITHUB_REF" == 'refs/heads/main' ]]; then
+ DeployOutput=$(vercel -t "$VERCEL_TOKEN" --prod)
+ else
+ DeployOutput=$(vercel -t "$VERCEL_TOKEN")
+ fi
+ echo "$DeployOutput"
+
+ ParsedURL=$(echo "$DeployOutput" | grep -Eo 'https://[^[:space:]]*\.vercel\.app' | tail -n 1)
+
+ if [[ -z "$ParsedURL" ]]; then
+ echo "Failed to parse Vercel URL from deploy output"
+ exit 1
+ fi
+ vercel inspect "$ParsedURL" -t "$VERCEL_TOKEN" -F json > vercel-inspect.json
+
+ InspectURL=$(jq -r '.url // empty' vercel-inspect.json)
+
+ if [[ -z "$InspectURL" ]]; then
+ echo "Failed to parse inspect url from vercel-inspect.json"
+ exit 1
+ fi
+ if [[ "$InspectURL" != http* ]]; then
+ InspectURL="https://$InspectURL"
+ fi
+ echo "preview-url=$InspectURL" >> "$GITHUB_OUTPUT"
```
### Docker Compose Usage
@@ -82,7 +111,7 @@
Supporting scripts in `.github/scripts` handle parsing, distribution, and aggregation logic. These workflows automate the reward process, ensuring transparency and reducing manual effort for both contributors and maintainers.
### Lark Notifications
-Lark (Feishu) notifications are integrated via GitHub Actions. After deployment, a notification is sent using the `foxundermoon/feishu-action@v2` action, with the webhook URL stored in the `LARK_CHATBOT_HOOK_URL` secret. The notification includes repository URL, branch, author, and preview deployment URL. Additionally, a dedicated workflow (`Lark-notification.yml`) sends notifications to Lark on various GitHub events (push, issues, pull requests, discussions, comments, releases). This workflow serializes event messages using a Deno script and sends them to Lark via webhook [(source)](https://github.com/Open-Source-Bazaar/Open-Source-Bazaar.github.io/blob/f5df98635f7a8bdab44cded44633ecbc3145a73f/.github/workflows/main.yml), [(source)](https://github.com/Open-Source-Bazaar/Open-Source-Bazaar.github.io/blob/f5df98635f7a8bdab44cded44633ecbc3145a73f/.github/workflows/Lark-notification.yml).
+Lark (Feishu) notifications are integrated via GitHub Actions. After deployment, a notification is sent using the `foxundermoon/feishu-action@v2` action, with the webhook URL stored in the `LARK_CHATBOT_HOOK_URL` secret. The notification uses an interactive card format (`msg_type: interactive`) with schema version 2.0, featuring a header with a blue template and markdown-formatted content in the body. The notification includes repository URL, branch, author, and preview deployment URL. Additionally, a dedicated workflow (`Lark-notification.yml`) sends notifications to Lark on various GitHub events (push, issues, pull requests, discussions, comments, releases). This workflow serializes event messages using a Deno script and sends them to Lark via webhook [(source)](https://github.com/Open-Source-Bazaar/Open-Source-Bazaar.github.io/blob/f5df98635f7a8bdab44cded44633ecbc3145a73f/.github/workflows/main.yml), [(source)](https://github.com/Open-Source-Bazaar/Open-Source-Bazaar.github.io/blob/f5df98635f7a8bdab44cded44633ecbc3145a73f/.github/workflows/Lark-notification.yml).
Example notification step:
```yaml
@@ -90,32 +119,24 @@
uses: foxundermoon/feishu-action@v2
with:
url: ${{ secrets.LARK_CHATBOT_HOOK_URL }}
- msg_type: post
+ msg_type: interactive
content: |
- post:
- zh_cn:
- title: Vercel 预览环境
- content:
- - - tag: text
- text: Git 仓库:
- - tag: a
- text: ${{ github.server_url }}/${{ github.repository }}
- href: ${{ github.server_url }}/${{ github.repository }}
- - - tag: text
- text: 代码分支:
- - tag: a
- text: ${{ github.ref }}
- href: ${{ github.server_url }}/${{ github.repository }}/tree/${{ github.ref_name }}
- - - tag: text
- text: 提交作者:
- - tag: a
- text: ${{ github.actor }}
- href: ${{ github.server_url }}/${{ github.actor }}
- - - tag: text
- text: 预览链接:
- - tag: a
- text: ${{ steps.vercel-deployment.outputs.preview-url }}
- href: ${{ steps.vercel-deployment.outputs.preview-url }}
+ schema: "2.0"
+ config:
+ wide_screen_mode: true
+ header:
+ title:
+ tag: plain_text
+ content: Vercel 部署通知
+ template: blue
+ body:
+ elements:
+ - tag: markdown
+ content: |
+ **Git 仓库:** [${{ github.server_url }}/${{ github.repository }}](${{ github.server_url }}/${{ github.repository }})
+ **代码分支:** [${{ github.ref }}](${{ github.server_url }}/${{ github.repository }}/tree/${{ github.ref_name }})
+ **提交作者:** [${{ github.actor }}](${{ github.server_url }}/${{ github.actor }})
+ **预览链接:** [${{ steps.vercel-deployment.outputs.preview-url }}](${{ steps.vercel-deployment.outputs.preview-url }})
```
This setup ensures robust automation, observability, and standardization across the development and deployment lifecycle. |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
models/configuration.ts (1)
9-17:SMTP_PORT默认值类型不一致,建议改为字符串'465'
process.env的所有值类型为string | undefined,而默认值465是number,导致 TypeScript 推断SMTP_PORT的类型为string | number。虽然消费方用+SMTP_PORT强转可以正常工作,但类型层面不够干净。♻️ 建议修改
- SMTP_PORT = 465, + SMTP_PORT = '465',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@models/configuration.ts` around lines 9 - 17, The default for SMTP_PORT is currently the number 465, causing SMTP_PORT to be typed as string | number; change the default to the string '465' in the destructuring of process.env (i.e., export const { SMTP_PORT = '465', ... } = process.env) so SMTP_PORT remains string | undefined and type-consistent with other env vars; update any call sites that relied on a numeric type (e.g., places using +SMTP_PORT or Number(SMTP_PORT)) if needed.pages/api/Lark/mail/[address]/message.ts (1)
23-23: 用Reflect.get读取body属性不够直观
Reflect.get(context.request, 'body')等价于(context.request as any).body,建议用更具可读性的写法,或直接通过类型扩展声明body属性。♻️ 建议修改
- const input = Reflect.get(context.request, 'body') as Mail.Options; + const input = (context.request as typeof context.request & { body: Mail.Options }).body;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pages/api/Lark/mail/`[address]/message.ts at line 23, 将使用 Reflect.get(context.request, 'body') 读取 body 的写法改为更直观的访问或通过类型扩展声明 body:直接使用 (context.request as { body: Mail.Options }).body 或 在请求上下文的类型定义中为 request 添加 body: Mail.Options,然后用 context.request.body 赋值给 const input。替换掉 Reflect.get 并确保 input 保持 Mail.Options 类型以提高可读性和类型安全(定位符:const input、Reflect.get、context.request、Mail.Options)。
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@pages/api/Lark/mail/`[address]/message.ts:
- Around line 22-26: 接口 router.post('/bot/message' ) 当前未做认证,允许任何请求调用
transporter.sendMail 导致 open relay 风险;在该路由前加入与项目其它 API(如 pages/api/signature
路由相同)的认证中间件(例如 JWT 验证中间件或校验函数),在处理函数内验证请求身份并拒绝未授权请求,再调用 transporter.sendMail({
...input, from: SMTP_USER });,确保未通过认证时返回 401/403 并且不触发发送邮件。
- Around line 22-26: The route handler router.post('/bot/message', async context
=> { ... }) ignores the dynamic [address] param—extract the dynamic address from
the request (e.g. context.params.address or context.request.params.address
depending on the router) and enforce it as the recipient: if address is missing
return a 400, if input.to exists either validate it matches the address or
override/merge with the address, then call transporter.sendMail with the
resolved to address (preserve SMTP_USER for from). Update the handler to
validate/normalize input (Mail.Options) against the extracted address and return
an error when there is a mismatch or when the address is required but absent.
- Line 25: Wrap the asynchronous call to transporter.sendMail in a try/catch
around the use of transporter.sendMail(...) so failures are caught and not
allowed to bubble up; on error, log the exception and set an appropriate HTTP
error response on context (do not leak stack traces). Also ensure the from
header is never undefined by providing a fallback when building the mail input
(use SMTP_USER || '<no-reply@example.com>' or derive from config) before calling
transporter.sendMail, and validate input to avoid sending invalid headers;
update references: transporter.sendMail, SMTP_USER, and context.body
accordingly.
---
Nitpick comments:
In `@models/configuration.ts`:
- Around line 9-17: The default for SMTP_PORT is currently the number 465,
causing SMTP_PORT to be typed as string | number; change the default to the
string '465' in the destructuring of process.env (i.e., export const { SMTP_PORT
= '465', ... } = process.env) so SMTP_PORT remains string | undefined and
type-consistent with other env vars; update any call sites that relied on a
numeric type (e.g., places using +SMTP_PORT or Number(SMTP_PORT)) if needed.
In `@pages/api/Lark/mail/`[address]/message.ts:
- Line 23: 将使用 Reflect.get(context.request, 'body') 读取 body 的写法改为更直观的访问或通过类型扩展声明
body:直接使用 (context.request as { body: Mail.Options }).body 或 在请求上下文的类型定义中为
request 添加 body: Mail.Options,然后用 context.request.body 赋值给 const input。替换掉
Reflect.get 并确保 input 保持 Mail.Options 类型以提高可读性和类型安全(定位符:const
input、Reflect.get、context.request、Mail.Options)。
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
.envis excluded by none and included by nonepackage.jsonis excluded by none and included by nonepnpm-lock.yamlis excluded by!**/pnpm-lock.yaml,!pnpm-lock.yamland included by none
📒 Files selected for processing (2)
models/configuration.tspages/api/Lark/mail/[address]/message.ts
There was a problem hiding this comment.
♻️ Duplicate comments (1)
pages/api/Lark/mail/[address]/message.ts (1)
23-26:⚠️ Potential issue | 🟠 Major动态路由参数
[address]未被使用,导致接口语义偏离路径设计Line 23-26 当前完全忽略
context.params.address,收件人仍由请求体控制。这样会让/api/Lark/mail/[address]/message这个路径约束失效,也偏离 issue#56的接口语义。建议将路径参数作为最终to(或严格校验二者一致)。建议修改
router.post('/bot/message', safeAPI, verifyJWT, async context => { - const input = Reflect.get(context.request, 'body') as Mail.Options; + const { address } = context.params ?? {}; + if (!address) context.throw(400, 'address is required'); + const input = context.request?.body as Mail.Options; - context.body = await transporter.sendMail({ ...input, from: SMTP_USER }); + if (input?.to && input.to !== address) { + context.throw(400, '`to` must match path param address'); + } + context.body = await transporter.sendMail({ + ...input, + to: address, + from: SMTP_USER, + }); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pages/api/Lark/mail/`[address]/message.ts around lines 23 - 26, The route handler registered with router.post('/bot/message') currently ignores the dynamic route param context.params.address and uses the request body for recipients; update the handler (in the async function that obtains input via Reflect.get(context.request, 'body') and calls transporter.sendMail) to either (a) override/assign input.to = context.params.address so the path-determined address is the final recipient, or (b) validate that input.to (or input.recipients) matches context.params.address and reject the request if they differ; ensure the change references the existing Mail.Options input, preserves SMTP_USER as the from address, and returns an appropriate error when validation fails.
🧹 Nitpick comments (2)
pages/api/Lark/mail/[address]/message.ts (2)
24-24: 避免使用Reflect.get读取请求体,改为可选链与直接属性访问Line 24 用
Reflect.get(context.request, 'body')会弱化可读性和类型约束。这里可直接使用context.request?.body。
As per coding guidelines, "Use optional chaining and modern ECMAScript features".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pages/api/Lark/mail/`[address]/message.ts at line 24, Replace the Reflect.get call used to populate input — currently written as Reflect.get(context.request, 'body') — with direct optional-chained property access (context.request?.body) and assign it to the existing input variable typed as Mail.Options; ensure you handle the possible undefined case (e.g., validate or provide a fallback/early return) where context.request or body may be missing so downstream code using input remains safe.
3-3:Line 3 的
Mail.Options类型标注,建议改为import type,避免不必要的运行时加载内部模块路径,降低对 Nodemailer 内部结构的耦合风险。建议修改
-import Mail from 'nodemailer/lib/mailer'; +import type Mail from 'nodemailer/lib/mailer';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pages/api/Lark/mail/`[address]/message.ts at line 3, The import of Mail is only used for types (Mail.Options); change the current value import "import Mail from 'nodemailer/lib/mailer'" to a type-only import (e.g., "import type Mail from 'nodemailer/lib/mailer'") so the Mail symbol is erased at runtime and avoids pulling nodemailer internals; update any usages like Mail.Options in the file to continue using the type and run type-checking to ensure no runtime dependencies remain.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@pages/api/Lark/mail/`[address]/message.ts:
- Around line 23-26: The route handler registered with
router.post('/bot/message') currently ignores the dynamic route param
context.params.address and uses the request body for recipients; update the
handler (in the async function that obtains input via
Reflect.get(context.request, 'body') and calls transporter.sendMail) to either
(a) override/assign input.to = context.params.address so the path-determined
address is the final recipient, or (b) validate that input.to (or
input.recipients) matches context.params.address and reject the request if they
differ; ensure the change references the existing Mail.Options input, preserves
SMTP_USER as the from address, and returns an appropriate error when validation
fails.
---
Nitpick comments:
In `@pages/api/Lark/mail/`[address]/message.ts:
- Line 24: Replace the Reflect.get call used to populate input — currently
written as Reflect.get(context.request, 'body') — with direct optional-chained
property access (context.request?.body) and assign it to the existing input
variable typed as Mail.Options; ensure you handle the possible undefined case
(e.g., validate or provide a fallback/early return) where context.request or
body may be missing so downstream code using input remains safe.
- Line 3: The import of Mail is only used for types (Mail.Options); change the
current value import "import Mail from 'nodemailer/lib/mailer'" to a type-only
import (e.g., "import type Mail from 'nodemailer/lib/mailer'") so the Mail
symbol is erased at runtime and avoids pulling nodemailer internals; update any
usages like Mail.Options in the file to continue using the type and run
type-checking to ensure no runtime dependencies remain.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
.github/workflows/Lark-notification.ymlis excluded by none and included by none.github/workflows/main.ymlis excluded by none and included by none
📒 Files selected for processing (1)
pages/api/Lark/mail/[address]/message.ts
Summary by CodeRabbit