refactor: reduce mailbox login change scope

This commit is contained in:
dreamhunter2333
2026-09-10 12:13:37 +08:00
parent 288634c774
commit 8baa92dfbe
18 changed files with 78 additions and 165 deletions
-1
View File
@@ -37,7 +37,6 @@
### Testing ### Testing
- test: |邮箱登录| 覆盖密码登录 JWT 有效期与续期、非法 token 类型,以及用户重置绑定邮箱密码的权限检查
- test: |E2E| 覆盖 D1 数据库大小响应、配置键隔离,以及数据库页面套餐选择的持久化与刷新恢复 - test: |E2E| 覆盖 D1 数据库大小响应、配置键隔离,以及数据库页面套餐选择的持久化与刷新恢复
- fix: |E2E| 覆盖发信页面草稿编辑、正文格式切换及 HTML 预览 - fix: |E2E| 覆盖发信页面草稿编辑、正文格式切换及 HTML 预览
- fix: |E2E| 覆盖用户 JWT 发信接口的地址归属、额度扣减、实际投递和发件箱操作,以及用户中心查看地址凭证、切换发件地址和按地址过滤发件箱的完整流程 - fix: |E2E| 覆盖用户 JWT 发信接口的地址归属、额度扣减、实际投递和发件箱操作,以及用户中心查看地址凭证、切换发件地址和按地址过滤发件箱的完整流程
-1
View File
@@ -37,7 +37,6 @@
### Testing ### Testing
- test: |Mailbox Login| Cover password JWT lifetime and renewal, invalid token types, and authorization for bound mailbox password resets
- test: |E2E| Cover the D1 database-size response, config-key isolation, and persistence of the database-page plan selection across reloads - test: |E2E| Cover the D1 database-size response, config-key isolation, and persistence of the database-page plan selection across reloads
- fix: |E2E| Cover draft editing, content-format switching, and HTML preview in the send-mail composer - fix: |E2E| Cover draft editing, content-format switching, and HTML preview in the send-mail composer
- fix: |E2E| Cover address ownership, balance decrement, delivery, and sent-item operations through the User JWT API, plus user-center credential display, sender switching, and sent-item filtering by address - fix: |E2E| Cover address ownership, balance decrement, delivery, and sent-item operations through the User JWT API, plus user-center credential display, sender switching, and sent-item filtering by address
+3 -9
View File
@@ -4,7 +4,6 @@ import User from '../views/User.vue'
import UserOauth2Callback from '../views/user/UserOauth2Callback.vue' import UserOauth2Callback from '../views/user/UserOauth2Callback.vue'
import i18n from '../i18n' import i18n from '../i18n'
import { useGlobalState } from '../store' import { useGlobalState } from '../store'
import { api } from '../api'
import { import {
DEFAULT_LOCALE, DEFAULT_LOCALE,
getBrowserLocales, getBrowserLocales,
@@ -13,7 +12,7 @@ import {
resolveSupportedLocale, resolveSupportedLocale,
} from '../i18n/utils' } from '../i18n/utils'
const { jwt, preferredLocale, openSettings } = useGlobalState() const { jwt, preferredLocale } = useGlobalState()
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
@@ -56,7 +55,7 @@ const router = createRouter({
] ]
}); });
router.beforeEach(async (to, from, next) => { router.beforeEach((to, from, next) => {
const routeLocale = resolveSupportedLocale(to.path.split('/')[1]) const routeLocale = resolveSupportedLocale(to.path.split('/')[1])
const resolvedLocale = routeLocale || DEFAULT_LOCALE const resolvedLocale = routeLocale || DEFAULT_LOCALE
i18n.global.locale.value = resolvedLocale i18n.global.locale.value = resolvedLocale
@@ -70,12 +69,7 @@ router.beforeEach(async (to, from, next) => {
if (Object.prototype.hasOwnProperty.call(to.query, 'jwt')) { if (Object.prototype.hasOwnProperty.call(to.query, 'jwt')) {
const jwtQuery = Array.isArray(to.query.jwt) ? to.query.jwt[0] : to.query.jwt const jwtQuery = Array.isArray(to.query.jwt) ? to.query.jwt[0] : to.query.jwt
if (typeof jwtQuery === 'string') { if (typeof jwtQuery === 'string') {
try { jwt.value = jwtQuery
const config = openSettings.value.fetched ? openSettings.value : await api.fetch('/open_api/settings');
if (!config.addressPasswordLoginOnly) jwt.value = jwtQuery;
} catch {
// Do not import a login link until the server policy is known.
}
} }
const query = { ...to.query } const query = { ...to.query }
delete query.jwt delete query.jwt
-1
View File
@@ -165,7 +165,6 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
text: 'Advanced Features', text: 'Advanced Features',
collapsed: false, collapsed: false,
items: [ items: [
{ text: 'Mailbox Password Login', link: 'feature/mailbox-password-login' },
{ text: 'AI Email Recognition', link: 'feature/ai-extract' }, { text: 'AI Email Recognition', link: 'feature/ai-extract' },
{ text: 'Configure Subdomain Email', link: 'feature/subdomain' }, { text: 'Configure Subdomain Email', link: 'feature/subdomain' },
{ text: 'Configure S3 Attachments', link: 'feature/s3-attachment' }, { text: 'Configure S3 Attachments', link: 'feature/s3-attachment' },
-1
View File
@@ -165,7 +165,6 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
text: '高级功能', text: '高级功能',
collapsed: false, collapsed: false,
items: [ items: [
{ text: '邮箱密码登录', link: 'feature/mailbox-password-login' },
{ text: 'AI 邮件识别', link: 'feature/ai-extract' }, { text: 'AI 邮件识别', link: 'feature/ai-extract' },
{ text: '配置子域名邮箱', link: 'feature/subdomain' }, { text: '配置子域名邮箱', link: 'feature/subdomain' },
{ text: '配置 S3 附件', link: 'feature/s3-attachment' }, { text: '配置 S3 附件', link: 'feature/s3-attachment' },
@@ -1,9 +1,5 @@
# Admin User Management # Admin User Management
::: info Mailbox password login
See [mailbox password login](./mailbox-password-login) for password-only login, login JWT renewal and bound-mailbox password resets.
:::
## User Management Page ## User Management Page
![admin-user-management](/feature/admin-user-management.png) ![admin-user-management](/feature/admin-user-management.png)
@@ -1,8 +1,28 @@
# Mail API # Mail API
::: info Mailbox password login ## Mailbox password login
See [mailbox password login](./mailbox-password-login) for password-only login, login JWT renewal and bound-mailbox password resets.
::: `ADDRESS_PASSWORD_LOGIN_ONLY` defaults to `false` and only takes effect with `ENABLE_ADDRESS_PASSWORD=true`. It rejects legacy credentials for login and API access, and hides credential displays and automatic login links. Legacy credential links also fail API authentication. Existing mailboxes without passwords need a bound user or administrator to set one; no database migration is needed.
- Password login issues a 30-day JWT with `type: "address_password_login"`, `address`, `address_id`, `iat`, and `exp`. Mailbox APIs retain `Authorization: Bearer <jwt>` and use middleware for authentication.
- `GET /api/settings` returns this login information, `send_balance`, and `new_address_token`. A valid JWT with less than 7 days remaining receives a new 30-day token; otherwise the field is `null`. Expired JWTs require login again, and legacy credentials cannot obtain new tokens.
- When loading settings, the frontend validates the new token with another settings request before replacing the current token. Ordinary requests do not refresh tokens. External clients should also save `new_address_token`; the switch rejects legacy credentials used directly by SMTP/IMAP and Agent clients.
- The local cache uses server-returned mailbox information without decoding JWTs and retains both login methods independently. Historical token-only entries display “Saved mailbox” until selected and validated. Password-only login hides identified legacy entries without deleting them.
- Mailbox creation and authorized access through user accounts, administrators, and Telegram issue mailbox JWTs according to the switch. Telegram KV stores separate permanent `telegram_binding` tokens, which mailbox APIs reject. Expiration is ignored only for historical stored bindings accessed after Telegram identity verification; tokens submitted for new bindings must pass mailbox authentication.
### Reset a bound mailbox password
With `ENABLE_ADDRESS_PASSWORD` enabled, the user center offers password reset without the previous password:
```http
POST /user_api/address/:address_id/reset_password
x-user-token: <user JWT>
Content-Type: application/json
{"new_password":"<64-character lowercase SHA-256 hex digest of the new password>"}
```
A single SQL statement checks that the user exists and owns the binding, updating only the existing password and update time. Success returns `{"success":true}`; missing authentication returns 401, an unbound mailbox or disabled feature returns 403, and invalid input returns 400. Resetting a password does not revoke existing JWTs; valid JWTs can still renew. No session table or revocation state is added.
## Viewing Emails via Mail API ## Viewing Emails via Mail API
@@ -1,48 +0,0 @@
# Password-only mailbox login
```toml
ENABLE_ADDRESS_PASSWORD = true
ADDRESS_PASSWORD_LOGIN_ONLY = true
```
The new switch defaults to `false` and only takes effect when mailbox passwords are enabled. It rejects legacy mailbox credentials, including direct API access, disables the credential login endpoint and `?jwt=` login links, and hides credential displays and automatic login links. Mailbox creation still displays the generated password. Users and administrators can still open mailboxes they are authorized to access and receive a new login JWT.
Existing mailboxes without passwords need a bound user or administrator to set one. Enabling passwords does not generate passwords for existing mailboxes. No database migration is required.
## Mailbox login JWT
Password login returns a `jwt` whose payload adds `type: "address_password_login"`, `iat`, and `exp` to the existing `address` and `address_id` fields. It lasts 30 days. Mailbox APIs continue using `Authorization: Bearer <jwt>`. Middleware validates the signature, type, expiration, and mailbox existence; business APIs keep using the same address fields.
`GET /api/settings` returns the current login information (`address`, `address_id`, and for password login, `type`, `iat`, and `exp`), sending balance, and `new_address_token`. When a valid login JWT has less than 7 days remaining, `new_address_token` contains a newly issued 30-day JWT; otherwise it is `null`.
The frontend follows the user login flow: `getSettings()` loads settings, validates the returned token with another settings request, then updates the current JWT and local mailbox cache. A renewal response cannot overwrite a different selected mailbox. Renewal runs when mailbox settings are loaded; ordinary API requests do not perform additional token checks or refreshes.
The local mailbox cache stores the token with the address and login type returned by `settings`; the frontend does not decode JWTs. Legacy credentials and password login JWTs are retained independently, updating the same address and type after successful login or renewal validation. Lists label the login method. Password-only login hides identified legacy credential entries without deleting them. Historical token-only entries initially appear as “Saved mailbox”; selecting and validating one fills in its information. The backend always rejects disabled credentials.
Expired JWTs cannot be renewed; log in again. Legacy credentials cannot obtain a new login JWT through `settings`. With the switch disabled, existing credentials retain their previous behavior, while password login still returns the new JWT format.
External clients keep the same APIs and headers. Clients using a new login JWT need to accept and save `settings.new_address_token`. Enabling the switch also rejects legacy credentials used directly against mailbox APIs by SMTP/IMAP, Agent, and other clients.
## Telegram bindings
After mailbox creation or binding, Telegram stores a non-expiring token with `type: "telegram_binding"` in KV. This token is internal to Telegram and is rejected by mailbox APIs. The Bot and Mini App authenticate the Telegram user before reading that user's bindings and checking that the mailbox still exists.
When the Mini App opens a mailbox, it issues a mailbox JWT under the current login policy. With password-only login enabled, that JWT lasts 30 days and follows the web renewal flow above. The KV binding does not need to rotate with the web JWT.
Existing KV bindings remain compatible: only internal Telegram binding verification ignores token expiration while validating the signature and mailbox identity. Tokens submitted to create a new binding still follow mailbox API validation, so disabled legacy credentials and expired JWTs cannot create new bindings. Unlinking or deleting a mailbox removes access through that Telegram binding.
## Reset a bound mailbox password
The user center mailbox list offers password reset without the previous mailbox password. This only requires `ENABLE_ADDRESS_PASSWORD`, independently of the password-only switch.
```http
POST /user_api/address/:address_id/reset_password
x-user-token: <user JWT>
Content-Type: application/json
{"new_password":"<64-character lowercase SHA-256 hex digest of the new password>"}
```
The same update statement checks that the user exists and currently owns the binding, and updates only the existing `password` and `updated_at` fields. Success returns `{"success":true}`. Missing user authentication returns 401; a mailbox not bound to the user or disabled passwords returns 403; invalid input returns 400.
Resetting a password does not revoke issued login JWTs; valid login JWTs can still renew. This implementation adds no session table or revocation state.
@@ -2,10 +2,6 @@
Try it here: [@cf_temp_mail_bot](https://t.me/cf_temp_mail_bot) Try it here: [@cf_temp_mail_bot](https://t.me/cf_temp_mail_bot)
::: info Mailbox login and bindings
Telegram uses non-expiring internal binding tokens. Web mailbox login JWTs are issued and renewed separately. See [mailbox password login](./mailbox-password-login#telegram-bindings).
:::
::: warning Note ::: warning Note
The default `worker.dev` domain certificate for worker is not supported by Telegram. Please use a custom domain when configuring Telegram Bot. The default `worker.dev` domain certificate for worker is not supported by Telegram. Please use a custom domain when configuring Telegram Bot.
::: :::
+1 -1
View File
@@ -50,7 +50,7 @@ When `ADMIN_API_IP_WHITELIST` is unset or empty, source IPs are not restricted.
| `ENABLE_AUTO_REPLY` | Text/JSON | Allow automatic email replies. Sender filter (`source_prefix`) supports three modes: empty to match all senders, prefix for `startsWith` matching, or `/regex/` syntax for regex matching (e.g. `/@example\.com$/`) | `true` | | `ENABLE_AUTO_REPLY` | Text/JSON | Allow automatic email replies. Sender filter (`source_prefix`) supports three modes: empty to match all senders, prefix for `startsWith` matching, or `/regex/` syntax for regex matching (e.g. `/@example\.com$/`) | `true` |
| `DEFAULT_SEND_BALANCE` | Text/JSON | Default email sending balance. When greater than `0`, it is auto-initialized when users open the settings page or send mail for the first time. Defaults to `0` if unset | `1` | | `DEFAULT_SEND_BALANCE` | Text/JSON | Default email sending balance. When greater than `0`, it is auto-initialized when users open the settings page or send mail for the first time. Defaults to `0` if unset | `1` |
| `ENABLE_ADDRESS_PASSWORD` | Text/JSON | Enable address password feature, when enabled, passwords will be auto-generated for new addresses, supports password login and modification | `true` | | `ENABLE_ADDRESS_PASSWORD` | Text/JSON | Enable address password feature, when enabled, passwords will be auto-generated for new addresses, supports password login and modification | `true` |
| `ADDRESS_PASSWORD_LOGIN_ONLY` | Text/JSON | Default `false`; only effective with mailbox passwords enabled. Rejects legacy credentials for login and API access, using renewable mailbox login JWTs. See [mailbox password login](./feature/mailbox-password-login). | `true` | | `ADDRESS_PASSWORD_LOGIN_ONLY` | Text/JSON | Default `false`; only effective with mailbox passwords enabled. Rejects legacy credentials for login and API access, using renewable mailbox login JWTs. See [mailbox password login](./feature/mail-api#mailbox-password-login). | `true` |
| `ENABLE_AGENT_EMAIL_INFO` | Text/JSON | Whether to show AI Agent access info in the frontend "Address Credentials & Connection Methods" dialog (Address JWT, parsed-mail APIs, skill link) | `true` | | `ENABLE_AGENT_EMAIL_INFO` | Text/JSON | Whether to show AI Agent access info in the frontend "Address Credentials & Connection Methods" dialog (Address JWT, parsed-mail APIs, skill link) | `true` |
| `SMTP_IMAP_PROXY_CONFIG` | JSON | Show SMTP/IMAP proxy connection info in the frontend "Address Credentials & Connection Methods" dialog; display-only, does not start the proxy service, which must be deployed separately | See example below | | `SMTP_IMAP_PROXY_CONFIG` | JSON | Show SMTP/IMAP proxy connection info in the frontend "Address Credentials & Connection Methods" dialog; display-only, does not start the proxy service, which must be deployed separately | See example below |
| `SEND_MAIL_DOMAINS` | JSON | Restrict which sender domains can use the `SEND_MAIL` binding; when unset or empty, all domains are allowed | `["example.com", "mail.example.com"]` | | `SEND_MAIL_DOMAINS` | JSON | Restrict which sender domains can use the `SEND_MAIL` binding; when unset or empty, all domains are allowed | `["example.com", "mail.example.com"]` |
@@ -1,9 +1,5 @@
# Admin 用户相关 # Admin 用户相关
::: info 邮箱密码登录
仅密码登录、登录 JWT 自动续期及绑定邮箱密码重置见[邮箱密码登录](./mailbox-password-login)。
:::
## 用户管理页面 ## 用户管理页面
![admin-user-management](/feature/admin-user-management.png) ![admin-user-management](/feature/admin-user-management.png)
@@ -1,8 +1,28 @@
# 查看邮件 API # 查看邮件 API
::: info 邮箱密码登录 ## 邮箱密码登录
仅密码登录、登录 JWT 自动续期及绑定邮箱密码重置见[邮箱密码登录](./mailbox-password-login)。
::: `ADDRESS_PASSWORD_LOGIN_ONLY` 默认 `false`,仅在 `ENABLE_ADDRESS_PASSWORD=true` 时生效。启用后,后端拒绝旧凭据登录及 API 访问,前端隐藏凭据和自动登录链接;旧凭据登录链接也无法通过 API 鉴权。历史无密码邮箱需由绑定用户或管理员设置密码,无需数据库迁移。
- 密码登录返回 `type: "address_password_login"``address``address_id``iat``exp` 的 JWT,有效期 30 天。邮箱 API 仍使用 `Authorization: Bearer <jwt>`,由中间件统一鉴权。
- `GET /api/settings` 返回上述登录信息、`send_balance``new_address_token`;有效 JWT 剩余不足 7 天时返回新签发的 30 天 token,否则为 `null`。已过期 JWT 必须重新登录,旧凭据不能换取新 token。
- 网页加载设置时使用新 token 再次请求 `settings`,验证成功后替换当前 token,普通请求不额外刷新。外部客户端也应保存 `new_address_token`SMTP/IMAP、Agent 使用旧凭据直接调用 API 同样受开关限制。
- 本地缓存使用后端返回的邮箱信息,不解码 JWT;两种登录方式独立保留。历史 token 缓存先显示“已保存邮箱”,选中并验证后补全名称。仅密码登录时隐藏已识别的旧凭据入口,保留缓存。
- 创建邮箱及从用户中心、管理员、Telegram 打开有权访问的邮箱时,按开关签发邮箱 JWT。Telegram KV 单独保存永久的 `telegram_binding` token,邮箱 API 拒绝该类型;仅对已验证 Telegram 身份后读取的历史绑定忽略过期时间,新绑定提交的 token 仍须通过邮箱鉴权。
### 重置绑定邮箱密码
启用 `ENABLE_ADDRESS_PASSWORD` 后,用户中心提供“重置密码”,不需要原密码:
```http
POST /user_api/address/:address_id/reset_password
x-user-token: <JWT>
Content-Type: application/json
{"new_password":"<64SHA-256>"}
```
后端在同一条 SQL 中检查用户存在及绑定关系,仅更新现有密码和更新时间。成功返回 `{"success":true}`;未登录返回 401,未绑定或功能关闭返回 403,输入错误返回 400。密码重置不撤销已有 JWT,有效 JWT 仍可续期;不新增会话表或撤销状态。
## 通过 邮件 API 查看邮件 ## 通过 邮件 API 查看邮件
@@ -1,48 +0,0 @@
# 仅允许邮箱密码登录
```toml
ENABLE_ADDRESS_PASSWORD = true
ADDRESS_PASSWORD_LOGIN_ONLY = true
```
新开关默认 `false`,只有启用邮箱密码时才生效。开启后,后端拒绝旧邮箱凭据,包括直接访问邮箱 API;关闭凭据登录接口和 `?jwt=` 登录入口,前端隐藏凭据及自动登录链接。创建邮箱后仍显示生成的邮箱密码,用户中心和管理员仍可打开有权访问的邮箱,并获得新登录 JWT。
历史邮箱如果没有密码,需要由绑定用户或管理员设置;启用密码功能不会自动为历史邮箱生成密码。不需要数据库迁移。
## 邮箱登录 JWT
密码登录返回的 `jwt` 使用新 payload,在原有 `address``address_id` 上增加 `type: "address_password_login"``iat``exp`。有效期为 30 天,邮箱 API 仍使用 `Authorization: Bearer <jwt>`,中间件统一校验签名、类型、有效期和邮箱是否存在,业务 API 使用原有的地址字段。
`GET /api/settings` 返回当前登录信息(`address``address_id`,密码登录还包含 `type``iat``exp`)、发信余额及 `new_address_token`。有效登录 JWT 剩余不足 7 天时,`new_address_token` 为新签发的 30 天 JWT;否则为 `null`
前端与用户登录采用相同流程:`getSettings()` 获取设置,使用返回的新 JWT 再次请求 `settings` 校验,成功后更新当前 JWT 和本地邮箱缓存。续期响应不会覆盖已经切换的邮箱。续期只在加载邮箱设置时处理,普通 API 请求不额外检查或刷新 JWT。
本地邮箱缓存保存 token 及 `settings` 返回的邮箱名称和登录类型,前端不解码 JWT。旧凭据与密码登录 JWT 独立保留,同邮箱、同类型在登录或续期校验成功后更新。列表标明登录方式,启用“只允许密码登录”后隐藏已识别的旧凭据入口,但保留缓存。历史缓存只有 token 时先显示“已保存邮箱”,选中且验证成功后补全信息;后端始终拒绝已禁用的凭据。
已过期 JWT 不能续期,需要重新登录;旧凭据不能通过 `settings` 换取新登录 JWT。开关关闭时,原有凭据仍按原逻辑使用,密码登录仍返回新格式 JWT。
外部客户端继续使用相同的 API 和请求头;使用新登录 JWT 时需接收并保存 `settings.new_address_token`。启用开关后,SMTP/IMAP、Agent 等客户端直接使用旧凭据访问邮箱 API 也会被拒绝。
## Telegram 绑定
Telegram 在创建或绑定邮箱后,在 KV 中保存 `type: "telegram_binding"` 的不过期 token。此 token 仅供 Telegram 内部使用,邮箱 API 不接受它。Bot 和 Mini App 先验证 Telegram 身份,再读取对应用户的绑定,并检查邮箱仍然存在。
Mini App 打开邮箱时,根据当前登录策略签发邮箱 JWT;启用仅密码登录后,该 JWT 有效期为 30 天,由网页沿用上述流程续期。KV 中的绑定不需要跟随网页 JWT 续期。
已有 KV 绑定兼容旧格式,仅在 Telegram 内部验证签名和邮箱身份时不检查 token 的过期时间。外部提交的绑定请求仍按邮箱 API 规则校验 JWT,不能使用已禁用的旧凭据或过期 JWT 新增绑定。解绑或删除邮箱后,原 Telegram 绑定不再授予访问权限。
## 用户重置绑定邮箱密码
用户中心邮箱列表提供“重置密码”,不需要邮箱原密码。此功能仅依赖 `ENABLE_ADDRESS_PASSWORD`,不要求开启仅密码登录。
```http
POST /user_api/address/:address_id/reset_password
x-user-token: <JWT>
Content-Type: application/json
{"new_password":"<64SHA-256>"}
```
后端在同一条更新语句中确认用户存在及当前绑定关系,只更新现有 `password``updated_at` 字段。成功返回 `{"success":true}`;未登录返回 401,邮箱不属于当前用户或密码功能关闭返回 403,输入格式错误返回 400。
密码重置只修改密码,不撤销已经签发的登录 JWT;有效登录 JWT 仍可续期。本方案不新增会话表或撤销状态。
@@ -2,10 +2,6 @@
试用地址:[@cf_temp_mail_bot](https://t.me/cf_temp_mail_bot) 试用地址:[@cf_temp_mail_bot](https://t.me/cf_temp_mail_bot)
::: info 邮箱登录与绑定
Telegram 内部绑定使用不过期 token,网页邮箱登录 JWT 单独签发和续期。详见[邮箱密码登录](./mailbox-password-login#telegram-绑定)。
:::
::: warning 注意 ::: warning 注意
worker 默认的 `worker.dev` 域名的证书是不被 telegram 支持的,配置 Telegram Bot 请使用自定义域名 worker 默认的 `worker.dev` 域名的证书是不被 telegram 支持的,配置 Telegram Bot 请使用自定义域名
::: :::
+1 -1
View File
@@ -50,7 +50,7 @@
| `ENABLE_AUTO_REPLY` | 文本/JSON | 允许自动回复邮件。发件人过滤(`source_prefix`)支持三种模式:留空匹配所有发件人、填写前缀进行 `startsWith` 匹配、使用 `/regex/` 语法进行正则匹配(如 `/@example\.com$/` | `true` | | `ENABLE_AUTO_REPLY` | 文本/JSON | 允许自动回复邮件。发件人过滤(`source_prefix`)支持三种模式:留空匹配所有发件人、填写前缀进行 `startsWith` 匹配、使用 `/regex/` 语法进行正则匹配(如 `/@example\.com$/` | `true` |
| `DEFAULT_SEND_BALANCE` | 文本/JSON | 默认发送邮件余额;当值大于 `0` 时,用户打开前端设置页或首次发送邮件时会自动初始化该额度。如果不设置,将为 `0` | `1` | | `DEFAULT_SEND_BALANCE` | 文本/JSON | 默认发送邮件余额;当值大于 `0` 时,用户打开前端设置页或首次发送邮件时会自动初始化该额度。如果不设置,将为 `0` | `1` |
| `ENABLE_ADDRESS_PASSWORD` | 文本/JSON | 启用邮箱地址密码功能,启用后创建新地址时会自动生成密码,并支持密码登录和修改 | `true` | | `ENABLE_ADDRESS_PASSWORD` | 文本/JSON | 启用邮箱地址密码功能,启用后创建新地址时会自动生成密码,并支持密码登录和修改 | `true` |
| `ADDRESS_PASSWORD_LOGIN_ONLY` | 文本/JSON | 默认 `false`,仅在启用邮箱密码时生效;禁用旧凭据登录及 API 访问,使用可自动续期的邮箱登录 JWT。见[邮箱密码登录](./feature/mailbox-password-login) | `true` | | `ADDRESS_PASSWORD_LOGIN_ONLY` | 文本/JSON | 默认 `false`,仅在启用邮箱密码时生效;禁用旧凭据登录及 API 访问,使用可自动续期的邮箱登录 JWT。见[邮箱密码登录](./feature/mail-api#邮箱密码登录) | `true` |
| `ENABLE_AGENT_EMAIL_INFO` | 文本/JSON | 是否在前端“地址凭证与连接方式”弹窗中展示 AI Agent 接入信息(Address JWT、parsed-mail API、skill 链接) | `true` | | `ENABLE_AGENT_EMAIL_INFO` | 文本/JSON | 是否在前端“地址凭证与连接方式”弹窗中展示 AI Agent 接入信息(Address JWT、parsed-mail API、skill 链接) | `true` |
| `SMTP_IMAP_PROXY_CONFIG` | JSON | 在前端“地址凭证与连接方式”弹窗中展示 SMTP/IMAP 代理连接信息;仅用于展示给用户,不会启动代理服务,代理服务仍需单独部署 | 见下方示例 | | `SMTP_IMAP_PROXY_CONFIG` | JSON | 在前端“地址凭证与连接方式”弹窗中展示 SMTP/IMAP 代理连接信息;仅用于展示给用户,不会启动代理服务,代理服务仍需单独部署 | 见下方示例 |
| `SEND_MAIL_DOMAINS` | JSON | 限制 `SEND_MAIL` binding 可用于哪些发件域名;留空或不配置时允许所有域名 | `["example.com", "mail.example.com"]` | | `SEND_MAIL_DOMAINS` | JSON | 限制 `SEND_MAIL` binding 可用于哪些发件域名;留空或不配置时允许所有域名 | `["example.com", "mail.example.com"]` |
-29
View File
@@ -1,29 +0,0 @@
import { Context } from 'hono';
import { getBooleanValue } from '../utils';
import i18n from '../i18n';
export const resetBoundAddressPassword = async (c: Context<HonoCustomType>) => {
const msgs = i18n.getMessagesbyContext(c);
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
return c.text(msgs.PasswordChangeDisabledMsg, 403);
}
const addressId = Number(c.req.param('address_id'));
const userId = c.get('userPayload')?.user_id;
if (!Number.isSafeInteger(addressId) || addressId <= 0 || !userId) {
return c.text(msgs.InvalidAddressOrUserTokenMsg, 400);
}
const body = await c.req.json<{ new_password?: unknown }>().catch(() => null);
if (typeof body?.new_password !== 'string' || !/^[a-f0-9]{64}$/.test(body.new_password)) {
return c.text(msgs.InvalidInputMsg, 400);
}
const result = await c.env.DB.prepare(
`UPDATE address SET password = ?, updated_at = datetime('now')
WHERE id = ? AND EXISTS (
SELECT 1 FROM users_address ua JOIN users u ON u.id = ua.user_id
WHERE ua.address_id = address.id AND ua.user_id = ?
)`
).bind(body.new_password, addressId, userId).run();
if (!result.success) return c.text(msgs.FailedUpdatePasswordMsg, 500);
if (result.meta.changes !== 1) return c.text(msgs.AddressNotBindedMsg, 403);
return c.json({ success: true });
};
+26 -1
View File
@@ -1,7 +1,7 @@
import { Context } from 'hono'; import { Context } from 'hono';
import { createAddressToken } from '../address_auth'; import { createAddressToken } from '../address_auth';
import { isAddressCountLimitReached } from "../utils" import { getBooleanValue, isAddressCountLimitReached } from "../utils"
import { unbindTelegramByAddress } from '../telegram_api/common'; import { unbindTelegramByAddress } from '../telegram_api/common';
import i18n from '../i18n'; import i18n from '../i18n';
import { updateAddressUpdatedAt, commonGetUserRole, handleListQuery, hideObjectFields } from '../common'; import { updateAddressUpdatedAt, commonGetUserRole, handleListQuery, hideObjectFields } from '../common';
@@ -23,6 +23,31 @@ export const getBindedAddressById = async (
} }
const UserBindAddressModule = { const UserBindAddressModule = {
resetPassword: async (c: Context<HonoCustomType>) => {
const msgs = i18n.getMessagesbyContext(c);
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
return c.text(msgs.PasswordChangeDisabledMsg, 403);
}
const addressId = Number(c.req.param('address_id'));
const userId = c.get('userPayload')?.user_id;
if (!Number.isSafeInteger(addressId) || addressId <= 0 || !userId) {
return c.text(msgs.InvalidAddressOrUserTokenMsg, 400);
}
const body = await c.req.json<{ new_password?: unknown }>().catch(() => null);
if (typeof body?.new_password !== 'string' || !/^[a-f0-9]{64}$/.test(body.new_password)) {
return c.text(msgs.InvalidInputMsg, 400);
}
const result = await c.env.DB.prepare(
`UPDATE address SET password = ?, updated_at = datetime('now')
WHERE id = ? AND EXISTS (
SELECT 1 FROM users_address ua JOIN users u ON u.id = ua.user_id
WHERE ua.address_id = address.id AND ua.user_id = ?
)`
).bind(body.new_password, addressId, userId).run();
if (!result.success) return c.text(msgs.FailedUpdatePasswordMsg, 500);
if (result.meta.changes !== 1) return c.text(msgs.AddressNotBindedMsg, 403);
return c.json({ success: true });
},
bind: async (c: Context<HonoCustomType>) => { bind: async (c: Context<HonoCustomType>) => {
const { user_id } = c.get("userPayload"); const { user_id } = c.get("userPayload");
const { address_id } = c.get("jwtPayload"); const { address_id } = c.get("jwtPayload");
+1 -2
View File
@@ -7,7 +7,6 @@ import passkey from './passkey';
import oauth2 from './oauth2'; import oauth2 from './oauth2';
import user_mail_api from './user_mail_api'; import user_mail_api from './user_mail_api';
import user_send_mail_api from './user_send_mail_api'; import user_send_mail_api from './user_send_mail_api';
import { resetBoundAddressPassword } from './address_password';
export const api = new Hono<HonoCustomType>(); export const api = new Hono<HonoCustomType>();
@@ -39,7 +38,7 @@ api.post('/user_api/oauth2/callback', oauth2.oauth2Login);
api.get('/user_api/bind_address', bind_address.getBindedAddresses); api.get('/user_api/bind_address', bind_address.getBindedAddresses);
api.post('/user_api/bind_address', bind_address.bind); api.post('/user_api/bind_address', bind_address.bind);
api.get('/user_api/bind_address_jwt/:address_id', bind_address.getBindedAddressJwt); api.get('/user_api/bind_address_jwt/:address_id', bind_address.getBindedAddressJwt);
api.post('/user_api/address/:address_id/reset_password', resetBoundAddressPassword); api.post('/user_api/address/:address_id/reset_password', bind_address.resetPassword);
api.post('/user_api/unbind_address', bind_address.unbind); api.post('/user_api/unbind_address', bind_address.unbind);
api.post('/user_api/transfer_address', bind_address.transferAddress); api.post('/user_api/transfer_address', bind_address.transferAddress);