mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-05 23:47:50 +08:00
feat(subdomain): add random second-level mailbox support (#924)
Summary: add random second-level subdomain mailbox creation for web, admin, and Telegram. Scope: worker config, UI toggle, and README/VitePress documentation. Co-authored-by: wufei <fwu@creams.io>
This commit is contained in:
@@ -109,6 +109,7 @@
|
|||||||
|
|
||||||
- [x] 使用 `rust wasm` 解析邮件,解析速度快,几乎所有邮件都能解析,node 的解析模块解析邮件失败的邮件,rust wasm 也能解析成功
|
- [x] 使用 `rust wasm` 解析邮件,解析速度快,几乎所有邮件都能解析,node 的解析模块解析邮件失败的邮件,rust wasm 也能解析成功
|
||||||
- [x] **AI 邮件识别** - 使用 Cloudflare Workers AI 自动提取邮件中的验证码、认证链接、服务链接等重要信息
|
- [x] **AI 邮件识别** - 使用 Cloudflare Workers AI 自动提取邮件中的验证码、认证链接、服务链接等重要信息
|
||||||
|
- [x] 支持为指定基础域名创建随机二级域名邮箱地址,更适合收件隔离场景
|
||||||
- [x] 支持发送邮件,支持 `DKIM` 验证
|
- [x] 支持发送邮件,支持 `DKIM` 验证
|
||||||
- [x] 支持 `SMTP` 和 `Resend` 等多种发送方式
|
- [x] 支持 `SMTP` 和 `Resend` 等多种发送方式
|
||||||
- [x] 增加查看 `附件` 功能,支持附件图片显示
|
- [x] 增加查看 `附件` 功能,支持附件图片显示
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ Try it now → [https://mail.awsl.uk/](https://mail.awsl.uk/)
|
|||||||
|
|
||||||
- [x] Use `rust wasm` to parse emails, with fast parsing speed. Almost all emails can be parsed. Even emails that Node.js parsing modules fail to parse can be successfully parsed by rust wasm
|
- [x] Use `rust wasm` to parse emails, with fast parsing speed. Almost all emails can be parsed. Even emails that Node.js parsing modules fail to parse can be successfully parsed by rust wasm
|
||||||
- [x] **AI Email Recognition** - Use Cloudflare Workers AI to automatically extract verification codes, authentication links, service links and other important information from emails
|
- [x] **AI Email Recognition** - Use Cloudflare Workers AI to automatically extract verification codes, authentication links, service links and other important information from emails
|
||||||
|
- [x] Support optional random second-level subdomain mailbox creation for selected base domains
|
||||||
- [x] Support sending emails with `DKIM` verification
|
- [x] Support sending emails with `DKIM` verification
|
||||||
- [x] Support multiple sending methods such as `SMTP` and `Resend`
|
- [x] Support multiple sending methods such as `SMTP` and `Resend`
|
||||||
- [x] Add attachment viewing feature with support for displaying attachment images
|
- [x] Add attachment viewing feature with support for displaying attachment images
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ const getOpenSettings = async (message, notification) => {
|
|||||||
maxAddressLen: res["maxAddressLen"] || 30,
|
maxAddressLen: res["maxAddressLen"] || 30,
|
||||||
needAuth: res["needAuth"] || false,
|
needAuth: res["needAuth"] || false,
|
||||||
defaultDomains: res["defaultDomains"] || [],
|
defaultDomains: res["defaultDomains"] || [],
|
||||||
|
randomSubdomainDomains: res["randomSubdomainDomains"] || [],
|
||||||
domains: res["domains"].map((domain, index) => {
|
domains: res["domains"].map((domain, index) => {
|
||||||
return {
|
return {
|
||||||
label: domainLabels.length > index ? domainLabels[index] : domain,
|
label: domainLabels.length > index ? domainLabels[index] : domain,
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export const useGlobalState = createGlobalState(
|
|||||||
enableIndexAbout: false,
|
enableIndexAbout: false,
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
defaultDomains: [],
|
defaultDomains: [],
|
||||||
|
/** @type {string[]} */
|
||||||
|
randomSubdomainDomains: [],
|
||||||
/** @type {Array<{label: string, value: string}>} */
|
/** @type {Array<{label: string, value: string}>} */
|
||||||
domains: [],
|
domains: [],
|
||||||
copyright: 'Dream Hunter',
|
copyright: 'Dream Hunter',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
@@ -22,6 +22,8 @@ const { t } = useI18n({
|
|||||||
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login to your email account.',
|
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login to your email account.',
|
||||||
addressPassword: 'Address Password',
|
addressPassword: 'Address Password',
|
||||||
linkWithAddressCredential: 'Open to auto login email link',
|
linkWithAddressCredential: 'Open to auto login email link',
|
||||||
|
enableRandomSubdomain: 'Use Random Subdomain',
|
||||||
|
randomSubdomainTip: 'When enabled, the created address will use a random subdomain. Subdomain addresses are recommended for receiving only.',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
address: '地址',
|
address: '地址',
|
||||||
@@ -33,11 +35,14 @@ const { t } = useI18n({
|
|||||||
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
||||||
addressPassword: '地址密码',
|
addressPassword: '地址密码',
|
||||||
linkWithAddressCredential: '打开即可自动登录邮箱的链接',
|
linkWithAddressCredential: '打开即可自动登录邮箱的链接',
|
||||||
|
enableRandomSubdomain: '启用随机子域名',
|
||||||
|
randomSubdomainTip: '启用后,创建出来的地址会自动挂在随机子域名下。子域名地址更建议仅用于收件。',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const enablePrefix = ref(true)
|
const enablePrefix = ref(true)
|
||||||
|
const enableRandomSubdomain = ref(false)
|
||||||
const emailName = ref("")
|
const emailName = ref("")
|
||||||
const emailDomain = ref("")
|
const emailDomain = ref("")
|
||||||
const showReultModal = ref(false)
|
const showReultModal = ref(false)
|
||||||
@@ -45,6 +50,19 @@ const result = ref("")
|
|||||||
const addressPassword = ref("")
|
const addressPassword = ref("")
|
||||||
const createdAddress = ref("")
|
const createdAddress = ref("")
|
||||||
|
|
||||||
|
const canUseRandomSubdomain = computed(() => {
|
||||||
|
if (!emailDomain.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return (openSettings.value.randomSubdomainDomains || []).includes(emailDomain.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(canUseRandomSubdomain, (enabled) => {
|
||||||
|
if (!enabled) {
|
||||||
|
enableRandomSubdomain.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const newEmail = async () => {
|
const newEmail = async () => {
|
||||||
if (!emailName.value || !emailDomain.value) {
|
if (!emailName.value || !emailDomain.value) {
|
||||||
message.error(t('fillInAllFields'))
|
message.error(t('fillInAllFields'))
|
||||||
@@ -55,6 +73,7 @@ const newEmail = async () => {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
enablePrefix: enablePrefix.value,
|
enablePrefix: enablePrefix.value,
|
||||||
|
enableRandomSubdomain: enableRandomSubdomain.value,
|
||||||
name: emailName.value,
|
name: emailName.value,
|
||||||
domain: emailDomain.value,
|
domain: emailDomain.value,
|
||||||
})
|
})
|
||||||
@@ -119,6 +138,14 @@ onMounted(async () => {
|
|||||||
:options="openSettings.domains" />
|
:options="openSettings.domains" />
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
|
<n-form-item-row v-if="canUseRandomSubdomain">
|
||||||
|
<n-checkbox v-model:checked="enableRandomSubdomain">
|
||||||
|
{{ t('enableRandomSubdomain') }}
|
||||||
|
</n-checkbox>
|
||||||
|
<p style="margin: 8px 0 0; opacity: 0.75;">
|
||||||
|
{{ t('randomSubdomainTip') }}
|
||||||
|
</p>
|
||||||
|
</n-form-item-row>
|
||||||
<n-button @click="newEmail" type="primary" block :loading="loading">
|
<n-button @click="newEmail" type="primary" block :loading="loading">
|
||||||
{{ t('creatNewEmail') }}
|
{{ t('creatNewEmail') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { NewLabelOutlined, EmailOutlined } from '@vicons/material'
|
import { NewLabelOutlined, EmailOutlined } from '@vicons/material'
|
||||||
@@ -19,13 +19,14 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
newAddressPath: {
|
newAddressPath: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: async (address_name, domain, cf_token) => {
|
default: async (address_name, domain, cf_token, enableRandomSubdomain) => {
|
||||||
return await api.fetch("/api/new_address", {
|
return await api.fetch("/api/new_address", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: address_name,
|
name: address_name,
|
||||||
domain: domain,
|
domain: domain,
|
||||||
cf_token: cf_token,
|
cf_token: cf_token,
|
||||||
|
enableRandomSubdomain: enableRandomSubdomain,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -47,6 +48,7 @@ const credential = ref('')
|
|||||||
const emailName = ref("")
|
const emailName = ref("")
|
||||||
const emailDomain = ref("")
|
const emailDomain = ref("")
|
||||||
const cfToken = ref("")
|
const cfToken = ref("")
|
||||||
|
const enableRandomSubdomain = ref(false)
|
||||||
const loginCfToken = ref("")
|
const loginCfToken = ref("")
|
||||||
const loginTurnstileRef = ref(null)
|
const loginTurnstileRef = ref(null)
|
||||||
const loginMethod = ref('credential') // 'credential' or 'password'
|
const loginMethod = ref('credential') // 'credential' or 'password'
|
||||||
@@ -141,6 +143,8 @@ const { locale, t } = useI18n({
|
|||||||
email: 'Email',
|
email: 'Email',
|
||||||
password: 'Password',
|
password: 'Password',
|
||||||
emailPasswordRequired: 'Email and password are required',
|
emailPasswordRequired: 'Email and password are required',
|
||||||
|
enableRandomSubdomain: 'Use Random Subdomain',
|
||||||
|
randomSubdomainTip: 'When enabled, the created address will use a random subdomain. Subdomain addresses are recommended for receiving only.',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
login: '登录',
|
login: '登录',
|
||||||
@@ -163,6 +167,8 @@ const { locale, t } = useI18n({
|
|||||||
email: '邮箱',
|
email: '邮箱',
|
||||||
password: '密码',
|
password: '密码',
|
||||||
emailPasswordRequired: '邮箱和密码不能为空',
|
emailPasswordRequired: '邮箱和密码不能为空',
|
||||||
|
enableRandomSubdomain: '启用随机子域名',
|
||||||
|
randomSubdomainTip: '启用后,创建出来的地址会自动挂在随机子域名下。子域名地址更建议仅用于收件。',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -215,7 +221,8 @@ const newEmail = async () => {
|
|||||||
const res = await props.newAddressPath(
|
const res = await props.newAddressPath(
|
||||||
nameToSend,
|
nameToSend,
|
||||||
emailDomain.value,
|
emailDomain.value,
|
||||||
cfToken.value
|
cfToken.value,
|
||||||
|
enableRandomSubdomain.value
|
||||||
);
|
);
|
||||||
jwt.value = res["jwt"];
|
jwt.value = res["jwt"];
|
||||||
addressPassword.value = res["password"] || '';
|
addressPassword.value = res["password"] || '';
|
||||||
@@ -241,6 +248,19 @@ const addressPrefix = computed(() => {
|
|||||||
return openSettings.value.prefix;
|
return openSettings.value.prefix;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const canUseRandomSubdomain = computed(() => {
|
||||||
|
if (!emailDomain.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (openSettings.value.randomSubdomainDomains || []).includes(emailDomain.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(canUseRandomSubdomain, (enabled) => {
|
||||||
|
if (!enabled) {
|
||||||
|
enableRandomSubdomain.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const domainsOptions = computed(() => {
|
const domainsOptions = computed(() => {
|
||||||
// if user has role, return role domains
|
// if user has role, return role domains
|
||||||
if (userSettings.value.user_role) {
|
if (userSettings.value.user_role) {
|
||||||
@@ -350,6 +370,14 @@ onMounted(async () => {
|
|||||||
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
|
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
|
||||||
:options="domainsOptions" />
|
:options="domainsOptions" />
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
|
<n-form-item-row v-if="canUseRandomSubdomain">
|
||||||
|
<n-checkbox v-model:checked="enableRandomSubdomain">
|
||||||
|
{{ t('enableRandomSubdomain') }}
|
||||||
|
</n-checkbox>
|
||||||
|
<p style="margin: 8px 0 0; opacity: 0.75;">
|
||||||
|
{{ t('randomSubdomainTip') }}
|
||||||
|
</p>
|
||||||
|
</n-form-item-row>
|
||||||
<Turnstile v-model:value="cfToken" />
|
<Turnstile v-model:value="cfToken" />
|
||||||
<n-button type="primary" block secondary strong @click="newEmail" :loading="loading">
|
<n-button type="primary" block secondary strong @click="newEmail" :loading="loading">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
|
|||||||
@@ -52,13 +52,19 @@ const fetchData = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newAddressPath = async (address_name: string, domain: string, cf_token: string) => {
|
const newAddressPath = async (
|
||||||
|
address_name: string,
|
||||||
|
domain: string,
|
||||||
|
cf_token: string,
|
||||||
|
enableRandomSubdomain: boolean
|
||||||
|
) => {
|
||||||
return await api.fetch("/telegram/new_address", {
|
return await api.fetch("/telegram/new_address", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
initData: telegramApp.value.initData,
|
initData: telegramApp.value.initData,
|
||||||
address: `${address_name}@${domain}`,
|
address: `${address_name}@${domain}`,
|
||||||
cf_token: cf_token,
|
cf_token: cf_token,
|
||||||
|
enableRandomSubdomain,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,3 +9,29 @@ Mail channel is no longer supported. The reference below is limited to the recei
|
|||||||
Reference
|
Reference
|
||||||
|
|
||||||
- [Configure Subdomain Email](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/164#issuecomment-2082612710)
|
- [Configure Subdomain Email](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/164#issuecomment-2082612710)
|
||||||
|
|
||||||
|
## Create Random Second-level Subdomain Addresses
|
||||||
|
|
||||||
|
If your base domain mail routing is already configured, you can also let users create mailbox
|
||||||
|
addresses with an automatically generated random second-level subdomain, for example:
|
||||||
|
|
||||||
|
- Base domain: `abc.com`
|
||||||
|
- Created address: `name@x7k2p9q1.abc.com`
|
||||||
|
|
||||||
|
This is useful for mailbox isolation and reducing repeated hits on the same address.
|
||||||
|
|
||||||
|
Add these worker variables:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
RANDOM_SUBDOMAIN_DOMAINS = ["abc.com"]
|
||||||
|
RANDOM_SUBDOMAIN_LENGTH = 8
|
||||||
|
```
|
||||||
|
|
||||||
|
- `RANDOM_SUBDOMAIN_DOMAINS`: base domains that allow optional random second-level subdomains
|
||||||
|
- `RANDOM_SUBDOMAIN_LENGTH`: random string length, range `1-63`, default `8`
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> This feature only appends a random second-level subdomain when the mailbox is created.
|
||||||
|
>
|
||||||
|
> It does not automatically create Cloudflare-side subdomain mail routes or DNS records for you,
|
||||||
|
> so make sure the base-domain/subdomain routing is already available first.
|
||||||
|
|||||||
@@ -32,11 +32,20 @@
|
|||||||
| `ADDRESS_REGEX` | Text | Regular expression to replace illegal symbols in `email address` name, symbols not in the regex will be replaced. Default is `[^a-z0-9]` if not set. Use with caution as some symbols may prevent email reception | `[^a-z0-9]` |
|
| `ADDRESS_REGEX` | Text | Regular expression to replace illegal symbols in `email address` name, symbols not in the regex will be replaced. Default is `[^a-z0-9]` if not set. Use with caution as some symbols may prevent email reception | `[^a-z0-9]` |
|
||||||
| `DEFAULT_DOMAINS` | JSON | Default domains available to users (not logged in or users without assigned roles) | `["awsl.uk", "dreamhunter2333.xyz"]` |
|
| `DEFAULT_DOMAINS` | JSON | Default domains available to users (not logged in or users without assigned roles) | `["awsl.uk", "dreamhunter2333.xyz"]` |
|
||||||
| `CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST` | Text/JSON | Whether to prioritize default domain when creating new addresses, if set to true, will use the first domain when no domain is specified, mainly for telegram bot scenarios | `false` |
|
| `CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST` | Text/JSON | Whether to prioritize default domain when creating new addresses, if set to true, will use the first domain when no domain is specified, mainly for telegram bot scenarios | `false` |
|
||||||
|
| `RANDOM_SUBDOMAIN_DOMAINS` | JSON | Base domains that allow optional random subdomain creation, so `name@abc.com` can become `name@<random>.abc.com` | `["abc.com"]` |
|
||||||
|
| `RANDOM_SUBDOMAIN_LENGTH` | Number | Random subdomain length, default `8`, valid range `1-63` | `8` |
|
||||||
| `DOMAIN_LABELS` | JSON | For Chinese domains, you can use DOMAIN_LABELS to display Chinese names | `["中文.awsl.uk", "dreamhunter2333.xyz"]` |
|
| `DOMAIN_LABELS` | JSON | For Chinese domains, you can use DOMAIN_LABELS to display Chinese names | `["中文.awsl.uk", "dreamhunter2333.xyz"]` |
|
||||||
| `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, will be 0 if not set | `1` |
|
| `DEFAULT_SEND_BALANCE` | Text/JSON | Default email sending balance, will be 0 if not set | `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` |
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> `RANDOM_SUBDOMAIN_DOMAINS` only controls automatic random subdomain generation during mailbox
|
||||||
|
> creation. It does not create Cloudflare-side subdomain routing for you.
|
||||||
|
>
|
||||||
|
> Subdomain addresses are usually best used for receiving only; for sending, prefer the main
|
||||||
|
> domain.
|
||||||
|
|
||||||
## Email Reception Related Variables
|
## Email Reception Related Variables
|
||||||
|
|
||||||
| Variable Name | Type | Description | Example |
|
| Variable Name | Type | Description | Example |
|
||||||
|
|||||||
@@ -9,3 +9,27 @@ mail channel 已不被支持,下面参考中仅限收件部分。
|
|||||||
参考
|
参考
|
||||||
|
|
||||||
- [配置子域名邮箱](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/164#issuecomment-2082612710)
|
- [配置子域名邮箱](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/164#issuecomment-2082612710)
|
||||||
|
|
||||||
|
## 创建随机二级域名地址
|
||||||
|
|
||||||
|
如果你已经配置好了基础域名的收件路由,还可以让用户在创建邮箱时,自动生成随机二级域名地址,例如:
|
||||||
|
|
||||||
|
- 基础域名:`abc.com`
|
||||||
|
- 创建结果:`name@x7k2p9q1.abc.com`
|
||||||
|
|
||||||
|
这适合做收件隔离、降低地址被重复命中的概率。
|
||||||
|
|
||||||
|
在 `worker` 变量中增加:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
RANDOM_SUBDOMAIN_DOMAINS = ["abc.com"]
|
||||||
|
RANDOM_SUBDOMAIN_LENGTH = 8
|
||||||
|
```
|
||||||
|
|
||||||
|
- `RANDOM_SUBDOMAIN_DOMAINS`:允许启用随机二级域名的基础域名列表
|
||||||
|
- `RANDOM_SUBDOMAIN_LENGTH`:随机串长度,范围 `1-63`,默认 `8`
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> 这个功能只是在“创建地址”时自动补一个随机二级域名。
|
||||||
|
>
|
||||||
|
> 它不会自动帮你创建 Cloudflare 侧的子域名收件路由或 DNS 配置,请先确保基础域名/子域名路由本身已经可用。
|
||||||
|
|||||||
@@ -32,11 +32,19 @@
|
|||||||
| `ADDRESS_REGEX` | 文本 | `邮箱名称` 替换非法符号的正则表达式, 不在其中的符号将被替换,如果不设置,默认为 `[^a-z0-9]`, 需谨慎使用, 有些符号可能导致无法收件 | `[^a-z0-9]` |
|
| `ADDRESS_REGEX` | 文本 | `邮箱名称` 替换非法符号的正则表达式, 不在其中的符号将被替换,如果不设置,默认为 `[^a-z0-9]`, 需谨慎使用, 有些符号可能导致无法收件 | `[^a-z0-9]` |
|
||||||
| `DEFAULT_DOMAINS` | JSON | 默认用户可用的域名(未登录或未分配角色的用户) | `["awsl.uk", "dreamhunter2333.xyz"]` |
|
| `DEFAULT_DOMAINS` | JSON | 默认用户可用的域名(未登录或未分配角色的用户) | `["awsl.uk", "dreamhunter2333.xyz"]` |
|
||||||
| `CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST` | 文本/JSON | 创建新地址时是否优先使用默认域名,如果设置为 true,当未指定域名时将使用第一个域名, 主要用于 telegram bot 场景 | `false` |
|
| `CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST` | 文本/JSON | 创建新地址时是否优先使用默认域名,如果设置为 true,当未指定域名时将使用第一个域名, 主要用于 telegram bot 场景 | `false` |
|
||||||
|
| `RANDOM_SUBDOMAIN_DOMAINS` | JSON | 允许启用随机子域名的基础域名列表,启用后可把 `name@abc.com` 创建成 `name@随机串.abc.com` | `["abc.com"]` |
|
||||||
|
| `RANDOM_SUBDOMAIN_LENGTH` | 数字 | 随机子域名长度,默认 `8`,范围 `1-63` | `8` |
|
||||||
| `DOMAIN_LABELS` | JSON | 对于中文域名,可以使用 DOMAIN_LABELS 显示域名的中文展示名称 | `["中文.awsl.uk", "dreamhunter2333.xyz"]` |
|
| `DOMAIN_LABELS` | JSON | 对于中文域名,可以使用 DOMAIN_LABELS 显示域名的中文展示名称 | `["中文.awsl.uk", "dreamhunter2333.xyz"]` |
|
||||||
| `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 | `1` |
|
| `DEFAULT_SEND_BALANCE` | 文本/JSON | 默认发送邮件余额,如果不设置,将为 0 | `1` |
|
||||||
| `ENABLE_ADDRESS_PASSWORD` | 文本/JSON | 启用邮箱地址密码功能,启用后创建新地址时会自动生成密码,并支持密码登录和修改 | `true` |
|
| `ENABLE_ADDRESS_PASSWORD` | 文本/JSON | 启用邮箱地址密码功能,启用后创建新地址时会自动生成密码,并支持密码登录和修改 | `true` |
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> `RANDOM_SUBDOMAIN_DOMAINS` 只负责“创建地址时自动补随机子域名”,不会自动帮你创建 Cloudflare
|
||||||
|
> 侧的子域名路由。
|
||||||
|
>
|
||||||
|
> 子域名地址通常更适合收件;如果要发件,仍建议优先使用主域名。
|
||||||
|
|
||||||
## 接受邮件相关变量
|
## 接受邮件相关变量
|
||||||
|
|
||||||
| 变量名 | 类型 | 说明 | 示例 |
|
| 变量名 | 类型 | 说明 | 示例 |
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ api.get('/admin/address', async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
api.post('/admin/new_address', async (c) => {
|
api.post('/admin/new_address', async (c) => {
|
||||||
const { name, domain, enablePrefix } = await c.req.json();
|
const { name, domain, enablePrefix, enableRandomSubdomain } = await c.req.json();
|
||||||
const msgs = i18n.getMessagesbyContext(c);
|
const msgs = i18n.getMessagesbyContext(c);
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return c.text(msgs.RequiredFieldMsg, 400)
|
return c.text(msgs.RequiredFieldMsg, 400)
|
||||||
@@ -53,6 +53,7 @@ api.post('/admin/new_address', async (c) => {
|
|||||||
try {
|
try {
|
||||||
const res = await newAddress(c, {
|
const res = await newAddress(c, {
|
||||||
name, domain, enablePrefix,
|
name, domain, enablePrefix,
|
||||||
|
enableRandomSubdomain: getBooleanValue(enableRandomSubdomain),
|
||||||
checkLengthByConfig: false,
|
checkLengthByConfig: false,
|
||||||
addressPrefix: null,
|
addressPrefix: null,
|
||||||
checkAllowDomains: false,
|
checkAllowDomains: false,
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export default {
|
|||||||
"SUBDOMAIN_FORWARD_ADDRESS_LIST": utils.getJsonObjectValue<SubdomainForwardAddressList[]>(c.env.SUBDOMAIN_FORWARD_ADDRESS_LIST),
|
"SUBDOMAIN_FORWARD_ADDRESS_LIST": utils.getJsonObjectValue<SubdomainForwardAddressList[]>(c.env.SUBDOMAIN_FORWARD_ADDRESS_LIST),
|
||||||
"DEFAULT_DOMAINS": utils.getDefaultDomains(c),
|
"DEFAULT_DOMAINS": utils.getDefaultDomains(c),
|
||||||
"DOMAINS": utils.getDomains(c),
|
"DOMAINS": utils.getDomains(c),
|
||||||
|
"RANDOM_SUBDOMAIN_DOMAINS": utils.getRandomSubdomainDomains(c),
|
||||||
|
"RANDOM_SUBDOMAIN_LENGTH": utils.getIntValue(c.env.RANDOM_SUBDOMAIN_LENGTH, 8),
|
||||||
"DOMAIN_LABELS": utils.getStringArray(c.env.DOMAIN_LABELS),
|
"DOMAIN_LABELS": utils.getStringArray(c.env.DOMAIN_LABELS),
|
||||||
|
|
||||||
"HAS_JWT_SECRET": !!utils.getStringValue(c.env.JWT_SECRET),
|
"HAS_JWT_SECRET": !!utils.getStringValue(c.env.JWT_SECRET),
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ api.get('/open_api/settings', async (c) => {
|
|||||||
"maxAddressLen": utils.getIntValue(c.env.MAX_ADDRESS_LEN, 30),
|
"maxAddressLen": utils.getIntValue(c.env.MAX_ADDRESS_LEN, 30),
|
||||||
"defaultDomains": utils.getDefaultDomains(c),
|
"defaultDomains": utils.getDefaultDomains(c),
|
||||||
"domains": utils.getDomains(c),
|
"domains": utils.getDomains(c),
|
||||||
|
"randomSubdomainDomains": utils.getRandomSubdomainDomains(c),
|
||||||
"domainLabels": utils.getStringArray(c.env.DOMAIN_LABELS),
|
"domainLabels": utils.getStringArray(c.env.DOMAIN_LABELS),
|
||||||
"needAuth": needAuth,
|
"needAuth": needAuth,
|
||||||
"adminContact": c.env.ADMIN_CONTACT,
|
"adminContact": c.env.ADMIN_CONTACT,
|
||||||
|
|||||||
+99
-46
@@ -2,13 +2,15 @@ import { Context } from 'hono';
|
|||||||
import { Jwt } from 'hono/utils/jwt'
|
import { Jwt } from 'hono/utils/jwt'
|
||||||
import { WorkerMailerOptions } from 'worker-mailer';
|
import { WorkerMailerOptions } from 'worker-mailer';
|
||||||
|
|
||||||
import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList, hashPassword, getJsonObjectValue } from './utils';
|
import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList, hashPassword, getJsonObjectValue, getRandomSubdomainDomains } from './utils';
|
||||||
import { unbindTelegramByAddress } from './telegram_api/common';
|
import { unbindTelegramByAddress } from './telegram_api/common';
|
||||||
import { CONSTANTS } from './constants';
|
import { CONSTANTS } from './constants';
|
||||||
import { AdminWebhookSettings, WebhookMail, WebhookSettings } from './models';
|
import { AdminWebhookSettings, WebhookMail, WebhookSettings } from './models';
|
||||||
import i18n from './i18n';
|
import i18n from './i18n';
|
||||||
|
|
||||||
const DEFAULT_NAME_REGEX = /[^a-z0-9]/g;
|
const DEFAULT_NAME_REGEX = /[^a-z0-9]/g;
|
||||||
|
const DEFAULT_RANDOM_SUBDOMAIN_LENGTH = 8;
|
||||||
|
const MAX_RANDOM_SUBDOMAIN_ATTEMPTS = 5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if send mail is enabled for a specific domain
|
* Check if send mail is enabled for a specific domain
|
||||||
@@ -66,6 +68,26 @@ export const generateRandomName = (c: Context<HonoCustomType>): string => {
|
|||||||
return fullName.substring(0, Math.min(fullName.length, maxLength));
|
return fullName.substring(0, Math.min(fullName.length, maxLength));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generateRandomSubdomain = (c: Context<HonoCustomType>): string => {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
const length = Math.min(
|
||||||
|
Math.max(getIntValue(c.env.RANDOM_SUBDOMAIN_LENGTH, DEFAULT_RANDOM_SUBDOMAIN_LENGTH), 1),
|
||||||
|
63
|
||||||
|
);
|
||||||
|
let subdomain = "";
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
subdomain += charset.charAt(Math.floor(Math.random() * charset.length));
|
||||||
|
}
|
||||||
|
return subdomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowRandomSubdomainForDomain = (
|
||||||
|
c: Context<HonoCustomType>,
|
||||||
|
domain: string
|
||||||
|
): boolean => {
|
||||||
|
return getRandomSubdomainDomains(c).includes(domain);
|
||||||
|
}
|
||||||
|
|
||||||
const checkNameRegex = (c: Context<HonoCustomType>, name: string) => {
|
const checkNameRegex = (c: Context<HonoCustomType>, name: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
try {
|
try {
|
||||||
@@ -148,12 +170,42 @@ const generatePasswordForAddress = async (
|
|||||||
return plainPassword;
|
return plainPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const insertAddressRecord = async (
|
||||||
|
c: Context<HonoCustomType>,
|
||||||
|
address: string,
|
||||||
|
sourceMeta: string | undefined | null,
|
||||||
|
msgs: ReturnType<typeof i18n.getMessagesbyContext>
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const result = await c.env.DB.prepare(
|
||||||
|
`INSERT INTO address(name, source_meta) VALUES(?, ?)`
|
||||||
|
).bind(address, sourceMeta).run();
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(msgs.FailedCreateAddressMsg)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const message = (e as Error).message;
|
||||||
|
// Fallback: source_meta field may not exist, try without it
|
||||||
|
if (message && message.includes("source_meta")) {
|
||||||
|
const result = await c.env.DB.prepare(
|
||||||
|
`INSERT INTO address(name) VALUES(?)`
|
||||||
|
).bind(address).run();
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(msgs.FailedCreateAddressMsg)
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const newAddress = async (
|
export const newAddress = async (
|
||||||
c: Context<HonoCustomType>,
|
c: Context<HonoCustomType>,
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
domain,
|
domain,
|
||||||
enablePrefix,
|
enablePrefix,
|
||||||
|
enableRandomSubdomain = false,
|
||||||
checkLengthByConfig = true,
|
checkLengthByConfig = true,
|
||||||
addressPrefix = null,
|
addressPrefix = null,
|
||||||
checkAllowDomains = true,
|
checkAllowDomains = true,
|
||||||
@@ -162,6 +214,7 @@ export const newAddress = async (
|
|||||||
}: {
|
}: {
|
||||||
name: string, domain: string | undefined | null,
|
name: string, domain: string | undefined | null,
|
||||||
enablePrefix: boolean,
|
enablePrefix: boolean,
|
||||||
|
enableRandomSubdomain?: boolean,
|
||||||
checkLengthByConfig?: boolean,
|
checkLengthByConfig?: boolean,
|
||||||
addressPrefix?: string | undefined | null,
|
addressPrefix?: string | undefined | null,
|
||||||
checkAllowDomains?: boolean,
|
checkAllowDomains?: boolean,
|
||||||
@@ -215,56 +268,56 @@ export const newAddress = async (
|
|||||||
if (!domain || !allowDomains.includes(domain)) {
|
if (!domain || !allowDomains.includes(domain)) {
|
||||||
throw new Error(msgs.InvalidDomainMsg)
|
throw new Error(msgs.InvalidDomainMsg)
|
||||||
}
|
}
|
||||||
// create address
|
if (enableRandomSubdomain && !allowRandomSubdomainForDomain(c, domain)) {
|
||||||
name = name + "@" + domain;
|
throw new Error(msgs.RandomSubdomainNotAllowedMsg)
|
||||||
try {
|
}
|
||||||
// Try insert with source_meta field first
|
|
||||||
const result = await c.env.DB.prepare(
|
const maxAttempts = enableRandomSubdomain ? MAX_RANDOM_SUBDOMAIN_ATTEMPTS : 1;
|
||||||
`INSERT INTO address(name, source_meta) VALUES(?, ?)`
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||||
).bind(name, sourceMeta).run();
|
const addressDomain = enableRandomSubdomain
|
||||||
if (!result.success) {
|
? `${generateRandomSubdomain(c)}.${domain}`
|
||||||
throw new Error(msgs.FailedCreateAddressMsg)
|
: domain;
|
||||||
}
|
const address = `${name}@${addressDomain}`;
|
||||||
await updateAddressUpdatedAt(c, name);
|
|
||||||
} catch (e) {
|
try {
|
||||||
const message = (e as Error).message;
|
await insertAddressRecord(c, address, sourceMeta, msgs);
|
||||||
// Fallback: source_meta field may not exist, try without it
|
await updateAddressUpdatedAt(c, address);
|
||||||
if (message && message.includes("source_meta")) {
|
|
||||||
const result = await c.env.DB.prepare(
|
const address_id = await c.env.DB.prepare(
|
||||||
`INSERT INTO address(name) VALUES(?)`
|
`SELECT id FROM address where name = ?`
|
||||||
).bind(name).run();
|
).bind(address).first<number>("id");
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(msgs.FailedCreateAddressMsg)
|
if (!address_id) {
|
||||||
|
throw new Error(msgs.FailedCreateAddressMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果启用地址密码功能,自动生成密码
|
||||||
|
const generatedPassword = await generatePasswordForAddress(c, address);
|
||||||
|
|
||||||
|
// create jwt
|
||||||
|
const jwt = await Jwt.sign({
|
||||||
|
address: address,
|
||||||
|
address_id: address_id
|
||||||
|
}, c.env.JWT_SECRET, "HS256")
|
||||||
|
return {
|
||||||
|
jwt: jwt,
|
||||||
|
address: address,
|
||||||
|
password: generatedPassword,
|
||||||
|
address_id: address_id,
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const message = (e as Error).message;
|
||||||
|
if (message && message.includes("UNIQUE")) {
|
||||||
|
if (enableRandomSubdomain && attempt < maxAttempts - 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new Error(msgs.AddressAlreadyExistsMsg)
|
||||||
}
|
}
|
||||||
await updateAddressUpdatedAt(c, name);
|
|
||||||
} else if (message && message.includes("UNIQUE")) {
|
|
||||||
throw new Error(msgs.AddressAlreadyExistsMsg)
|
|
||||||
} else {
|
|
||||||
throw new Error(msgs.FailedCreateAddressMsg)
|
throw new Error(msgs.FailedCreateAddressMsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const address_id = await c.env.DB.prepare(
|
|
||||||
`SELECT id FROM address where name = ?`
|
|
||||||
).bind(name).first<number>("id");
|
|
||||||
|
|
||||||
if (!address_id) {
|
throw new Error(msgs.FailedCreateAddressMsg)
|
||||||
throw new Error(msgs.FailedCreateAddressMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果启用地址密码功能,自动生成密码
|
|
||||||
const generatedPassword = await generatePasswordForAddress(c, name);
|
|
||||||
|
|
||||||
// create jwt
|
|
||||||
const jwt = await Jwt.sign({
|
|
||||||
address: name,
|
|
||||||
address_id: address_id
|
|
||||||
}, c.env.JWT_SECRET, "HS256")
|
|
||||||
return {
|
|
||||||
jwt: jwt,
|
|
||||||
address: name,
|
|
||||||
password: generatedPassword,
|
|
||||||
address_id: address_id,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkNameBlockList = async (
|
const checkNameBlockList = async (
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ const messages: LocaleMessages = {
|
|||||||
NameTooShortMsg: "Name is too short",
|
NameTooShortMsg: "Name is too short",
|
||||||
NameTooLongMsg: "Name is too long",
|
NameTooLongMsg: "Name is too long",
|
||||||
InvalidDomainMsg: "Invalid domain",
|
InvalidDomainMsg: "Invalid domain",
|
||||||
|
RandomSubdomainNotAllowedMsg: "Random subdomain is not enabled for this domain",
|
||||||
AddressAlreadyExistsMsg: "Address already exists",
|
AddressAlreadyExistsMsg: "Address already exists",
|
||||||
MaxAddressCountReachedMsg: "Max address count reached",
|
MaxAddressCountReachedMsg: "Max address count reached",
|
||||||
AddressNotBindedMsg: "Address is not binded",
|
AddressNotBindedMsg: "Address is not binded",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export type LocaleMessages = {
|
|||||||
NameTooShortMsg: string
|
NameTooShortMsg: string
|
||||||
NameTooLongMsg: string
|
NameTooLongMsg: string
|
||||||
InvalidDomainMsg: string
|
InvalidDomainMsg: string
|
||||||
|
RandomSubdomainNotAllowedMsg: string
|
||||||
AddressAlreadyExistsMsg: string
|
AddressAlreadyExistsMsg: string
|
||||||
MaxAddressCountReachedMsg: string
|
MaxAddressCountReachedMsg: string
|
||||||
AddressNotBindedMsg: string
|
AddressNotBindedMsg: string
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ const messages: LocaleMessages = {
|
|||||||
NameTooShortMsg: "名称太短",
|
NameTooShortMsg: "名称太短",
|
||||||
NameTooLongMsg: "名称太长",
|
NameTooLongMsg: "名称太长",
|
||||||
InvalidDomainMsg: "无效的域名",
|
InvalidDomainMsg: "无效的域名",
|
||||||
|
RandomSubdomainNotAllowedMsg: "当前域名未启用随机子域名",
|
||||||
AddressAlreadyExistsMsg: "邮箱地址已存在",
|
AddressAlreadyExistsMsg: "邮箱地址已存在",
|
||||||
MaxAddressCountReachedMsg: "已达到最大地址数量限制",
|
MaxAddressCountReachedMsg: "已达到最大地址数量限制",
|
||||||
AddressNotBindedMsg: "邮箱地址未绑定",
|
AddressNotBindedMsg: "邮箱地址未绑定",
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ api.post('/api/new_address', async (c) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line prefer-const
|
// eslint-disable-next-line prefer-const
|
||||||
let { name, domain, cf_token } = await c.req.json();
|
let { name, domain, cf_token, enableRandomSubdomain } = await c.req.json();
|
||||||
// check cf turnstile
|
// check cf turnstile
|
||||||
try {
|
try {
|
||||||
await checkCfTurnstile(c, cf_token);
|
await checkCfTurnstile(c, cf_token);
|
||||||
@@ -160,6 +160,7 @@ api.post('/api/new_address', async (c) => {
|
|||||||
const res = await newAddress(c, {
|
const res = await newAddress(c, {
|
||||||
name, domain,
|
name, domain,
|
||||||
enablePrefix: true,
|
enablePrefix: true,
|
||||||
|
enableRandomSubdomain: getBooleanValue(enableRandomSubdomain),
|
||||||
checkLengthByConfig: true,
|
checkLengthByConfig: true,
|
||||||
addressPrefix,
|
addressPrefix,
|
||||||
sourceMeta
|
sourceMeta
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { LocaleMessages } from "../i18n/type";
|
|||||||
|
|
||||||
export const tgUserNewAddress = async (
|
export const tgUserNewAddress = async (
|
||||||
c: Context<HonoCustomType>, userId: string, address: string,
|
c: Context<HonoCustomType>, userId: string, address: string,
|
||||||
msgs: LocaleMessages
|
msgs: LocaleMessages,
|
||||||
|
enableRandomSubdomain: boolean = false
|
||||||
): Promise<{ address: string, jwt: string, password?: string | null }> => {
|
): Promise<{ address: string, jwt: string, password?: string | null }> => {
|
||||||
if (c.env.RATE_LIMITER) {
|
if (c.env.RATE_LIMITER) {
|
||||||
const { success } = await c.env.RATE_LIMITER.limit(
|
const { success } = await c.env.RATE_LIMITER.limit(
|
||||||
@@ -41,6 +42,7 @@ export const tgUserNewAddress = async (
|
|||||||
name: finalName,
|
name: finalName,
|
||||||
domain,
|
domain,
|
||||||
enablePrefix: true,
|
enablePrefix: true,
|
||||||
|
enableRandomSubdomain,
|
||||||
sourceMeta: `tg:${userId}`
|
sourceMeta: `tg:${userId}`
|
||||||
});
|
});
|
||||||
// for mail push to telegram
|
// for mail push to telegram
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Context } from "hono";
|
|||||||
import { Jwt } from 'hono/utils/jwt'
|
import { Jwt } from 'hono/utils/jwt'
|
||||||
import { CONSTANTS } from "../constants";
|
import { CONSTANTS } from "../constants";
|
||||||
import { bindTelegramAddress, jwtListToAddressData, tgUserNewAddress, unbindTelegramAddress } from "./common";
|
import { bindTelegramAddress, jwtListToAddressData, tgUserNewAddress, unbindTelegramAddress } from "./common";
|
||||||
import { checkCfTurnstile, checkIsAdmin } from "../utils";
|
import { checkCfTurnstile, checkIsAdmin, getBooleanValue } from "../utils";
|
||||||
import { TelegramSettings } from "./settings";
|
import { TelegramSettings } from "./settings";
|
||||||
import i18n from "../i18n";
|
import i18n from "../i18n";
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ async function getTelegramBindAddress(c: Context<HonoCustomType>): Promise<Respo
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function newTelegramAddress(c: Context<HonoCustomType>): Promise<Response> {
|
async function newTelegramAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||||
const { initData, address, cf_token } = await c.req.json();
|
const { initData, address, cf_token, enableRandomSubdomain } = await c.req.json();
|
||||||
const msgs = i18n.getMessagesbyContext(c);
|
const msgs = i18n.getMessagesbyContext(c);
|
||||||
// check cf turnstile
|
// check cf turnstile
|
||||||
try {
|
try {
|
||||||
@@ -94,7 +94,13 @@ async function newTelegramAddress(c: Context<HonoCustomType>): Promise<Response>
|
|||||||
try {
|
try {
|
||||||
const userId = await checkTelegramAuth(c, initData);
|
const userId = await checkTelegramAuth(c, initData);
|
||||||
// get the address list from the KV
|
// get the address list from the KV
|
||||||
const res = await tgUserNewAddress(c, userId, address, msgs)
|
const res = await tgUserNewAddress(
|
||||||
|
c,
|
||||||
|
userId,
|
||||||
|
address,
|
||||||
|
msgs,
|
||||||
|
getBooleanValue(enableRandomSubdomain)
|
||||||
|
)
|
||||||
return c.json(res);
|
return c.json(res);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
|
|||||||
Vendored
+2
@@ -25,6 +25,8 @@ type Bindings = {
|
|||||||
MAX_ADDRESS_LEN: string | number | undefined
|
MAX_ADDRESS_LEN: string | number | undefined
|
||||||
DEFAULT_DOMAINS: string | string[] | undefined
|
DEFAULT_DOMAINS: string | string[] | undefined
|
||||||
DOMAINS: string | string[] | undefined
|
DOMAINS: string | string[] | undefined
|
||||||
|
RANDOM_SUBDOMAIN_DOMAINS: string | string[] | undefined
|
||||||
|
RANDOM_SUBDOMAIN_LENGTH: string | number | undefined
|
||||||
DISABLE_CUSTOM_ADDRESS_NAME: string | boolean | undefined
|
DISABLE_CUSTOM_ADDRESS_NAME: string | boolean | undefined
|
||||||
CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST: string | boolean | undefined
|
CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST: string | boolean | undefined
|
||||||
ADMIN_USER_ROLE: string | undefined
|
ADMIN_USER_ROLE: string | undefined
|
||||||
|
|||||||
@@ -150,6 +150,13 @@ export const getDomains = (c: Context<HonoCustomType>): string[] => {
|
|||||||
return c.env.DOMAINS;
|
return c.env.DOMAINS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getRandomSubdomainDomains = (c: Context<HonoCustomType>): string[] => {
|
||||||
|
if (!c.env.RANDOM_SUBDOMAIN_DOMAINS) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return getStringArray(c.env.RANDOM_SUBDOMAIN_DOMAINS);
|
||||||
|
}
|
||||||
|
|
||||||
export const getUserRoles = (c: Context<HonoCustomType>): UserRole[] => {
|
export const getUserRoles = (c: Context<HonoCustomType>): UserRole[] => {
|
||||||
if (!c.env.USER_ROLES) {
|
if (!c.env.USER_ROLES) {
|
||||||
return [];
|
return [];
|
||||||
@@ -368,6 +375,7 @@ export default {
|
|||||||
getStringArray,
|
getStringArray,
|
||||||
getDefaultDomains,
|
getDefaultDomains,
|
||||||
getDomains,
|
getDomains,
|
||||||
|
getRandomSubdomainDomains,
|
||||||
getUserRoles,
|
getUserRoles,
|
||||||
getAnotherWorkerList,
|
getAnotherWorkerList,
|
||||||
getPasswords,
|
getPasswords,
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ PREFIX = "tmp"
|
|||||||
# CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST = false
|
# CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST = false
|
||||||
DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # domain name for no role users
|
DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # domain name for no role users
|
||||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # all domain names
|
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # all domain names
|
||||||
|
# Allow optional random subdomain generation for the listed base domains
|
||||||
|
# e.g. name@abc.com => name@r4nd0m.abc.com
|
||||||
|
# RANDOM_SUBDOMAIN_DOMAINS = ["abc.com"]
|
||||||
|
# RANDOM_SUBDOMAIN_LENGTH = 8
|
||||||
# For chinese domain name, you can use DOMAIN_LABELS to show chinese domain name
|
# For chinese domain name, you can use DOMAIN_LABELS to show chinese domain name
|
||||||
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
|
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
|
||||||
# USER_DEFAULT_ROLE = "vip" # default role for new users(only when enable mail verification)
|
# USER_DEFAULT_ROLE = "vip" # default role for new users(only when enable mail verification)
|
||||||
|
|||||||
Reference in New Issue
Block a user