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:
tsymr
2026-04-02 23:13:10 +08:00
committed by GitHub
parent be1bf71a47
commit db93828a81
24 changed files with 273 additions and 57 deletions

View File

@@ -74,6 +74,7 @@ const getOpenSettings = async (message, notification) => {
maxAddressLen: res["maxAddressLen"] || 30,
needAuth: res["needAuth"] || false,
defaultDomains: res["defaultDomains"] || [],
randomSubdomainDomains: res["randomSubdomainDomains"] || [],
domains: res["domains"].map((domain, index) => {
return {
label: domainLabels.length > index ? domainLabels[index] : domain,

View File

@@ -28,6 +28,8 @@ export const useGlobalState = createGlobalState(
enableIndexAbout: false,
/** @type {string[]} */
defaultDomains: [],
/** @type {string[]} */
randomSubdomainDomains: [],
/** @type {Array<{label: string, value: string}>} */
domains: [],
copyright: 'Dream Hunter',

View File

@@ -1,5 +1,5 @@
<script setup>
import { onMounted, ref } from 'vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'
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.',
addressPassword: 'Address Password',
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: {
address: '地址',
@@ -33,11 +35,14 @@ const { t } = useI18n({
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
addressPassword: '地址密码',
linkWithAddressCredential: '打开即可自动登录邮箱的链接',
enableRandomSubdomain: '启用随机子域名',
randomSubdomainTip: '启用后,创建出来的地址会自动挂在随机子域名下。子域名地址更建议仅用于收件。',
}
}
});
const enablePrefix = ref(true)
const enableRandomSubdomain = ref(false)
const emailName = ref("")
const emailDomain = ref("")
const showReultModal = ref(false)
@@ -45,6 +50,19 @@ const result = ref("")
const addressPassword = 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 () => {
if (!emailName.value || !emailDomain.value) {
message.error(t('fillInAllFields'))
@@ -55,6 +73,7 @@ const newEmail = async () => {
method: 'POST',
body: JSON.stringify({
enablePrefix: enablePrefix.value,
enableRandomSubdomain: enableRandomSubdomain.value,
name: emailName.value,
domain: emailDomain.value,
})
@@ -119,6 +138,14 @@ onMounted(async () => {
:options="openSettings.domains" />
</n-input-group>
</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">
{{ t('creatNewEmail') }}
</n-button>

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { NewLabelOutlined, EmailOutlined } from '@vicons/material'
@@ -19,13 +19,14 @@ const props = defineProps({
},
newAddressPath: {
type: Function,
default: async (address_name, domain, cf_token) => {
default: async (address_name, domain, cf_token, enableRandomSubdomain) => {
return await api.fetch("/api/new_address", {
method: "POST",
body: JSON.stringify({
name: address_name,
domain: domain,
cf_token: cf_token,
enableRandomSubdomain: enableRandomSubdomain,
}),
});
},
@@ -47,6 +48,7 @@ const credential = ref('')
const emailName = ref("")
const emailDomain = ref("")
const cfToken = ref("")
const enableRandomSubdomain = ref(false)
const loginCfToken = ref("")
const loginTurnstileRef = ref(null)
const loginMethod = ref('credential') // 'credential' or 'password'
@@ -141,6 +143,8 @@ const { locale, t } = useI18n({
email: 'Email',
password: 'Password',
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: {
login: '登录',
@@ -163,6 +167,8 @@ const { locale, t } = useI18n({
email: '邮箱',
password: '密码',
emailPasswordRequired: '邮箱和密码不能为空',
enableRandomSubdomain: '启用随机子域名',
randomSubdomainTip: '启用后,创建出来的地址会自动挂在随机子域名下。子域名地址更建议仅用于收件。',
}
}
});
@@ -215,7 +221,8 @@ const newEmail = async () => {
const res = await props.newAddressPath(
nameToSend,
emailDomain.value,
cfToken.value
cfToken.value,
enableRandomSubdomain.value
);
jwt.value = res["jwt"];
addressPassword.value = res["password"] || '';
@@ -241,6 +248,19 @@ const addressPrefix = computed(() => {
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(() => {
// if user has role, return role domains
if (userSettings.value.user_role) {
@@ -350,6 +370,14 @@ onMounted(async () => {
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
:options="domainsOptions" />
</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" />
<n-button type="primary" block secondary strong @click="newEmail" :loading="loading">
<template #icon>

View File

@@ -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", {
method: "POST",
body: JSON.stringify({
initData: telegramApp.value.initData,
address: `${address_name}@${domain}`,
cf_token: cf_token,
enableRandomSubdomain,
}),
});
}