diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d015f74..7d38da99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## main(v1.0.5) +- feat: 新增 `DISABLE_CUSTOM_ADDRESS_NAME` 配置: 禁用自定义邮箱地址名称功能 +- feat: 新增 `CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST` 配置: 创建地址时优先使用第一个域名 - feat: |UI| 主页增加进入极简模式按钮 ## v1.0.4 diff --git a/README.md b/README.md index 815cb457..ebc959d6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# 🚀 Cloudflare 临时邮箱 - 免费搭建临时邮件服务 +# Cloudflare 临时邮箱 - 免费搭建临时邮件服务

@@ -81,7 +81,7 @@

📖 目录(点击收缩/展开) -- [🚀 Cloudflare 临时邮箱 - 免费搭建临时邮件服务](#-cloudflare-临时邮箱---免费搭建临时邮件服务) +- [Cloudflare 临时邮箱 - 免费搭建临时邮件服务](#cloudflare-临时邮箱---免费搭建临时邮件服务) - [📚 部署文档 - 快速开始](#-部署文档---快速开始) - [📝 更新日志](#-更新日志) - [🎯 在线体验](#-在线体验) diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 2c2bc550..2e1bfb7a 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -78,6 +78,7 @@ const getOpenSettings = async (message, notification) => { adminContact: res["adminContact"] || "", enableUserCreateEmail: res["enableUserCreateEmail"] || false, disableAnonymousUserCreateEmail: res["disableAnonymousUserCreateEmail"] || false, + disableCustomAddressName: res["disableCustomAddressName"] || false, enableUserDeleteEmail: res["enableUserDeleteEmail"] || false, enableAutoReply: res["enableAutoReply"] || false, enableIndexAbout: res["enableIndexAbout"] || false, diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index 6f67ef88..a5a18f7f 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -22,6 +22,7 @@ export const useGlobalState = createGlobalState( adminContact: '', enableUserCreateEmail: false, disableAnonymousUserCreateEmail: false, + disableCustomAddressName: false, enableUserDeleteEmail: false, enableAutoReply: false, enableIndexAbout: false, diff --git a/frontend/src/views/common/Login.vue b/frontend/src/views/common/Login.vue index 9fae197d..77187b3c 100644 --- a/frontend/src/views/common/Login.vue +++ b/frontend/src/views/common/Login.vue @@ -84,6 +84,7 @@ const { locale, t } = useI18n({ credentialInput: 'Please input the Mail Address Credential', bindUserInfo: 'Logged in user, login without binding email or create new email address will bind to current user', bindUserAddressError: 'Error when bind email address to user', + autoGeneratedName: 'Auto-generated name', }, zh: { login: '登录', @@ -100,6 +101,7 @@ const { locale, t } = useI18n({ credentialInput: '请输入邮箱地址凭据', bindUserInfo: '已登录用户, 登录未绑定邮箱或创建新邮箱地址将绑定到当前用户', bindUserAddressError: '绑定邮箱地址到用户时错误', + autoGeneratedName: '自动生成名称', } } }); @@ -147,8 +149,10 @@ const generateName = async () => { const newEmail = async () => { try { + // If custom names are disabled, send empty name to trigger backend auto-generation + const nameToSend = openSettings.value.disableCustomAddressName ? "" : emailName.value; const res = await props.newAddressPath( - emailName.value, + nameToSend, emailDomain.value, cfToken.value ); @@ -240,19 +244,20 @@ onMounted(async () => { -

{{ t("getNewEmailTip1") + addressRegex.source }}

-

{{ t("getNewEmailTip2") }}

+

{{ t("getNewEmailTip1") + addressRegex.source }}

+

{{ t("getNewEmailTip2") }}

{{ t("getNewEmailTip3") }}

- + {{ t('generateName') }} {{ addressPrefix }} - + @ diff --git a/vitepress-docs/docs/zh/guide/feature/new-address-api.md b/vitepress-docs/docs/zh/guide/feature/new-address-api.md index 88cd3687..f80fbd46 100644 --- a/vitepress-docs/docs/zh/guide/feature/new-address-api.md +++ b/vitepress-docs/docs/zh/guide/feature/new-address-api.md @@ -24,9 +24,9 @@ res = requests.post( print(res.json()) ``` -# 批量创建随机用户名邮箱地址 API 示例 +## 批量创建随机用户名邮箱地址 API 示例 -## 通过 admin API 批量新建邮箱地址 +### 通过 admin API 批量新建邮箱地址 这是一个 `python` 的例子,使用 `requests` 库发送邮件。 diff --git a/vitepress-docs/docs/zh/guide/worker-vars.md b/vitepress-docs/docs/zh/guide/worker-vars.md index 7911cb66..d5c61e11 100644 --- a/vitepress-docs/docs/zh/guide/worker-vars.md +++ b/vitepress-docs/docs/zh/guide/worker-vars.md @@ -22,17 +22,19 @@ ## 邮箱相关变量 -| 变量名 | 类型 | 说明 | 示例 | -| ---------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | -| `PREFIX` | 文本 | 新建 `邮箱名称` 的默认前缀,不需要前缀可不配置 | `tmp` | -| `MIN_ADDRESS_LEN` | 数字 | `邮箱名称` 的最小长度 | `1` | -| `MAX_ADDRESS_LEN` | 数字 | `邮箱名称` 的最大长度 | `30` | -| `ADDRESS_CHECK_REGEX` | 文本 | `邮箱名称` 的正则表达式, 只用于检查 | `^(?!.*admin).*` | -| `ADDRESS_REGEX` | 文本 | `邮箱名称` 替换非法符号的正则表达式, 不在其中的符号将被替换,如果不设置,默认为 `[^a-z0-9]`, 需谨慎使用, 有些符号可能导致无法收件 | `[^a-z0-9]` | -| `DEFAULT_DOMAINS` | JSON | 默认用户可用的域名(未登录或未分配角色的用户) | `["awsl.uk", "dreamhunter2333.xyz"]` | -| `DOMAIN_LABELS` | JSON | 对于中文域名,可以使用 DOMAIN_LABELS 显示域名的中文展示名称 | `["中文.awsl.uk", "dreamhunter2333.xyz"]` | -| `ENABLE_AUTO_REPLY` | 文本/JSON | 允许自动回复邮件 | `true` | -| `DEFAULT_SEND_BALANCE` | 文本/JSON | 默认发送邮件余额,如果不设置,将为 0 | `1` | +| 变量名 | 类型 | 说明 | 示例 | +| ------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | +| `PREFIX` | 文本 | 新建 `邮箱名称` 的默认前缀,不需要前缀可不配置 | `tmp` | +| `MIN_ADDRESS_LEN` | 数字 | `邮箱名称` 的最小长度 | `1` | +| `MAX_ADDRESS_LEN` | 数字 | `邮箱名称` 的最大长度 | `30` | +| `DISABLE_CUSTOM_ADDRESS_NAME` | 文本/JSON | 禁用自定义邮箱地址名称,如果设置为 true,则用户无法输入自定义邮箱名称,将由后台自动生成 | `true` | +| `ADDRESS_CHECK_REGEX` | 文本 | `邮箱名称` 的正则表达式, 只用于检查 | `^(?!.*admin).*` | +| `ADDRESS_REGEX` | 文本 | `邮箱名称` 替换非法符号的正则表达式, 不在其中的符号将被替换,如果不设置,默认为 `[^a-z0-9]`, 需谨慎使用, 有些符号可能导致无法收件 | `[^a-z0-9]` | +| `DEFAULT_DOMAINS` | JSON | 默认用户可用的域名(未登录或未分配角色的用户) | `["awsl.uk", "dreamhunter2333.xyz"]` | +| `CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST` | 文本/JSON | 创建新地址时是否优先使用默认域名,如果设置为 true,当未指定域名时将使用第一个域名, 主要用于 telegram bot 场景 | `false` | +| `DOMAIN_LABELS` | JSON | 对于中文域名,可以使用 DOMAIN_LABELS 显示域名的中文展示名称 | `["中文.awsl.uk", "dreamhunter2333.xyz"]` | +| `ENABLE_AUTO_REPLY` | 文本/JSON | 允许自动回复邮件 | `true` | +| `DEFAULT_SEND_BALANCE` | 文本/JSON | 默认发送邮件余额,如果不设置,将为 0 | `1` | ## 接受邮件相关变量 diff --git a/worker/src/commom_api.ts b/worker/src/commom_api.ts index b2447ca5..2d6520aa 100644 --- a/worker/src/commom_api.ts +++ b/worker/src/commom_api.ts @@ -14,6 +14,7 @@ api.get('/open_api/settings', async (c) => { const auth = c.req.raw.headers.get("x-custom-auth"); needAuth = !auth || !passwords.includes(auth); } + return c.json({ "title": c.env.TITLE, "announcement": utils.getStringValue(c.env.ANNOUNCEMENT), @@ -29,6 +30,7 @@ api.get('/open_api/settings', async (c) => { "adminContact": c.env.ADMIN_CONTACT, "enableUserCreateEmail": utils.getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL), "disableAnonymousUserCreateEmail": utils.getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL), + "disableCustomAddressName": utils.getBooleanValue(c.env.DISABLE_CUSTOM_ADDRESS_NAME), "enableUserDeleteEmail": utils.getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL), "enableAutoReply": utils.getBooleanValue(c.env.ENABLE_AUTO_REPLY), "enableIndexAbout": utils.getBooleanValue(c.env.ENABLE_INDEX_ABOUT), diff --git a/worker/src/common.ts b/worker/src/common.ts index c70e72e9..9e4ae030 100644 --- a/worker/src/common.ts +++ b/worker/src/common.ts @@ -8,6 +8,31 @@ import { AdminWebhookSettings, WebhookMail, WebhookSettings } from './models'; const DEFAULT_NAME_REGEX = /[^a-z0-9]/g; +export const generateRandomName = (c: Context): string => { + // name min length min 1 + const minLength = Math.max( + getIntValue(c.env.MIN_ADDRESS_LEN, 1), + 1 + ); + // name max length min 1 + const maxLength = Math.max( + getIntValue(c.env.MAX_ADDRESS_LEN, 30), + 1 + ); + + // Build full name recursively until minimum length is reached + const buildName = (currentName: string = ""): string => { + return currentName.length >= minLength + ? currentName + : buildName(currentName + Math.random().toString(36).substring(2, 15)); + }; + + const fullName = buildName(); + + // Return truncated to max length + return fullName.substring(0, Math.min(fullName.length, maxLength)); +}; + const checkNameRegex = (c: Context, name: string) => { let error = null; try { @@ -108,9 +133,14 @@ export const newAddress = async ( } // check domain const allowDomains = checkAllowDomains ? await getAllowDomains(c) : getDomains(c); - // if domain is not set, use the random domain + // if domain is not set, select domain based on environment configuration if (!domain && allowDomains.length > 0) { - domain = allowDomains[Math.floor(Math.random() * allowDomains.length)]; + const createAddressDefaultDomainFirst = getBooleanValue(c.env.CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST); + if (createAddressDefaultDomainFirst) { + domain = allowDomains[0]; + } else { + domain = allowDomains[Math.floor(Math.random() * allowDomains.length)]; + } } // check domain is valid if (!domain || !allowDomains.includes(domain)) { diff --git a/worker/src/mails_api/index.ts b/worker/src/mails_api/index.ts index 63611172..dc2b75a0 100644 --- a/worker/src/mails_api/index.ts +++ b/worker/src/mails_api/index.ts @@ -2,7 +2,7 @@ import { Context, Hono } from 'hono' import i18n from '../i18n'; import { getBooleanValue, getJsonSetting, checkCfTurnstile, getStringValue, getSplitStringListValue } from '../utils'; -import { newAddress, handleListQuery, deleteAddressWithData, getAddressPrefix, getAllowDomains, updateAddressUpdatedAt } from '../common' +import { newAddress, handleListQuery, deleteAddressWithData, getAddressPrefix, getAllowDomains, updateAddressUpdatedAt, generateRandomName } from '../common' import { CONSTANTS } from '../constants' import auto_reply from './auto_reply' import webhook_settings from './webhook_settings'; @@ -123,9 +123,13 @@ api.post('/api/new_address', async (c) => { } catch (error) { return c.text(msgs.TurnstileCheckFailedMsg, 500) } - // if no name, generate random name - if (!name) { - name = Math.random().toString(36).substring(2, 15); + // Check if custom email names are disabled from environment variable + const disableCustomAddressName = getBooleanValue(c.env.DISABLE_CUSTOM_ADDRESS_NAME); + + // if no name or custom names are disabled, generate random name + if (!name || disableCustomAddressName) { + // Generate random name with context-based length configuration + name = generateRandomName(c); } // check name block list try { diff --git a/worker/src/telegram_api/common.ts b/worker/src/telegram_api/common.ts index 8e7abf66..51534052 100644 --- a/worker/src/telegram_api/common.ts +++ b/worker/src/telegram_api/common.ts @@ -1,8 +1,8 @@ import { Context } from "hono"; import { Jwt } from "hono/utils/jwt"; import { CONSTANTS } from "../constants"; -import { getIntValue, getJsonSetting } from "../utils"; -import { deleteAddressWithData, newAddress } from "../common"; +import { getBooleanValue, getIntValue, getJsonSetting } from "../utils"; +import { deleteAddressWithData, newAddress, generateRandomName } from "../common"; export const tgUserNewAddress = async ( c: Context, userId: string, address: string @@ -15,21 +15,27 @@ export const tgUserNewAddress = async ( throw Error("Rate limit exceeded") } } - // @ts-ignore - address = address || Math.random().toString(36).substring(2, 15); + // Check if custom address names are disabled + const disableCustomAddressName = getBooleanValue(c.env.DISABLE_CUSTOM_ADDRESS_NAME); + + // Parse address parameter const [name, domain] = address.includes("@") ? address.split("@") : [address, null]; const jwtList = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || []; if (jwtList.length >= getIntValue(c.env.TG_MAX_ADDRESS, 5)) { throw Error("绑定地址数量已达上限"); } + // Generate name if disabled or not provided + const finalName = (!name || disableCustomAddressName) ? generateRandomName(c) : name; + // check name block list const value = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY); const blockList = (value || []) as string[]; - if (blockList.some((item) => name.includes(item))) { - throw Error(`Name[${name}]is blocked`); + if (blockList.some((item) => finalName.includes(item))) { + throw Error(`Name[${finalName}]is blocked`); } + const res = await newAddress(c, { - name: name || Math.random().toString(36).substring(2, 15), + name: finalName, domain, enablePrefix: true }); diff --git a/worker/src/telegram_api/telegram.ts b/worker/src/telegram_api/telegram.ts index c5303587..ce7fd773 100644 --- a/worker/src/telegram_api/telegram.ts +++ b/worker/src/telegram_api/telegram.ts @@ -18,7 +18,7 @@ const COMMANDS = [ }, { command: "new", - description: "新建邮箱地址, 如果要自定义邮箱地址, 请输入 /new @, name [a-z0-9] 有效" + description: "新建邮箱地址, 如果要自定义邮箱地址, 请输入 /new, 通过 /new @ 可以指定, name [a-z0-9] 有效, name 为空则随机生成, @ 可选" }, { command: "address", diff --git a/worker/src/types.d.ts b/worker/src/types.d.ts index 2c4ceb41..e559d5b2 100644 --- a/worker/src/types.d.ts +++ b/worker/src/types.d.ts @@ -24,6 +24,8 @@ type Bindings = { MAX_ADDRESS_LEN: string | number | undefined DEFAULT_DOMAINS: string | string[] | undefined DOMAINS: string | string[] | undefined + DISABLE_CUSTOM_ADDRESS_NAME: string | boolean | undefined + CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST: string | boolean | undefined ADMIN_USER_ROLE: string | undefined USER_DEFAULT_ROLE: string | UserRole | undefined USER_ROLES: string | UserRole[] | undefined diff --git a/worker/wrangler.toml.template b/worker/wrangler.toml.template index 9d94f839..32bdcc61 100644 --- a/worker/wrangler.toml.template +++ b/worker/wrangler.toml.template @@ -35,6 +35,8 @@ PREFIX = "tmp" # (min, max) length of the adderss, if not set, the default is (1, 30) # MIN_ADDRESS_LEN = 1 # MAX_ADDRESS_LEN = 30 +# Disable custom email address name, if set true, users cannot input custom email name, will auto generate +# DISABLE_CUSTOM_ADDRESS_NAME = true # IF YOU WANT TO MAKE YOUR SITE PRIVATE, UNCOMMENT THE FOLLOWING LINES # PASSWORDS = ["123", "456"] # For admin panel @@ -43,6 +45,8 @@ PREFIX = "tmp" # DISABLE_ADMIN_PASSWORD_CHECK = false # ADMIN CONTACT, CAN BE ANY STRING # ADMIN_CONTACT = "xx@xx.xxx" +# Create new address with default domain first, if set true, will use first domain from DEFAULT_DOMAINS when no domain specified +# CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST = false DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # domain name for no role users DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # all domain names # For chinese domain name, you can use DOMAIN_LABELS to show chinese domain name