Compare commits

..
2 Commits
Author SHA1 Message Date
dreamhunter2333andClaude 8e5293f010 fix: enhance input validation with trim() for address creation
- Add trim() handling in newAddress() function to prevent whitespace issues
- Add trim() handling for address prefixes to ensure consistent formatting
- Add trim() handling in Telegram API address parsing for robustness
- Prevents edge cases with whitespace-only or padded input strings

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-04 02:06:53 +08:00
dreamhunter2333 711378cb11 feat: add var DISABLE_CUSTOM_ADDRESS_NAME and CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST 2025-09-04 01:59:15 +08:00
51 changed files with 2329 additions and 3527 deletions
+1 -8
View File
@@ -33,17 +33,10 @@ jobs:
- name: Deploy Backend for ${{ github.ref_name }} - name: Deploy Backend for ${{ github.ref_name }}
run: | run: |
export use_worker_assets=${{ secrets.USE_WORKER_ASSETS }} export use_worker_assets=${{ secrets.USE_WORKER_ASSETS }}
export use_worker_assets_with_telegram=${{ secrets.USE_WORKER_ASSETS_WITH_TELEGRAM }}
if [ -n "$use_worker_assets" ]; then if [ -n "$use_worker_assets" ]; then
cd frontend/ cd frontend/
pnpm install --no-frozen-lockfile pnpm install --no-frozen-lockfile
if [ -n "$use_worker_assets_with_telegram" ]; then
echo "Building with telegram pages"
pnpm build:telegram:pages
else
echo "Building with normal pages"
pnpm build:pages pnpm build:pages
fi
cd .. cd ..
fi fi
@@ -60,7 +53,7 @@ jobs:
echo "Applied mail-parser-wasm-worker patch" echo "Applied mail-parser-wasm-worker patch"
fi fi
if [ "$debug_mode" = "true" ]; then if [ -n "$debug_mode" ]; then
pnpm run deploy pnpm run deploy
else else
output=$(pnpm run deploy 2>&1) output=$(pnpm run deploy 2>&1)
+1 -11
View File
@@ -1,21 +1,11 @@
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 --> <!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
# CHANGE LOG # CHANGE LOG
## v1.0.6 ## main(v1.0.5)
- feat: |DB| update db schema add index
- feat: |地址密码| 增加地址密码登录功能, 通过 `ENABLE_ADDRESS_PASSWORD` 配置启用, 需要执行 `db/2025-09-23-patch.sql` 文件中的 SQL 更新 `D1` 数据库
- fix: |GitHub Actions| 修复 debug 模式配置,仅当 DEBUG_MODE 为 'true' 时才启用调试模式
- feat: |Admin| 账户管理页面新增多选批量操作功能(批量删除、批量清空收件箱、批量清空发件箱)
- feat: |Admin| 维护页面增加清理未绑定用户地址的功能
- feat: 支持针对角色配置不同的绑定地址数量上限, 可在 admin 页面配置
## v1.0.5
- feat: 新增 `DISABLE_CUSTOM_ADDRESS_NAME` 配置: 禁用自定义邮箱地址名称功能 - feat: 新增 `DISABLE_CUSTOM_ADDRESS_NAME` 配置: 禁用自定义邮箱地址名称功能
- feat: 新增 `CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST` 配置: 创建地址时优先使用第一个域名 - feat: 新增 `CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST` 配置: 创建地址时优先使用第一个域名
- feat: |UI| 主页增加进入极简模式按钮 - feat: |UI| 主页增加进入极简模式按钮
- feat: |Webhook| 增加白名单开关功能,支持灵活控制访问权限
## v1.0.4 ## v1.0.4
-1
View File
@@ -40,7 +40,6 @@
- 🆓 **完全免费** - 基于 Cloudflare 免费服务构建,零成本运行 - 🆓 **完全免费** - 基于 Cloudflare 免费服务构建,零成本运行
-**高性能** - Rust WASM 邮件解析,响应速度极快 -**高性能** - Rust WASM 邮件解析,响应速度极快
- 🎨 **现代化界面** - 响应式设计,支持多语言,操作简便 - 🎨 **现代化界面** - 响应式设计,支持多语言,操作简便
- 🔐 **地址密码** - 支持为邮箱地址设置独立密码,增强安全性 (通过 `ENABLE_ADDRESS_PASSWORD` 启用)
## 📚 部署文档 - 快速开始 ## 📚 部署文档 - 快速开始
-4
View File
@@ -1,4 +0,0 @@
ALTER TABLE
address
ADD
password TEXT;
-9
View File
@@ -9,22 +9,15 @@ CREATE TABLE IF NOT EXISTS raw_mails (
CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address); CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address);
CREATE INDEX IF NOT EXISTS idx_raw_mails_created_at ON raw_mails(created_at);
CREATE TABLE IF NOT EXISTS address ( CREATE TABLE IF NOT EXISTS address (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE, name TEXT UNIQUE,
password TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX IF NOT EXISTS idx_address_name ON address(name); CREATE INDEX IF NOT EXISTS idx_address_name ON address(name);
CREATE INDEX IF NOT EXISTS idx_address_created_at ON address(created_at);
CREATE INDEX IF NOT EXISTS idx_address_updated_at ON address(updated_at);
CREATE TABLE IF NOT EXISTS auto_reply_mails ( CREATE TABLE IF NOT EXISTS auto_reply_mails (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
source_prefix TEXT, source_prefix TEXT,
@@ -57,8 +50,6 @@ CREATE TABLE IF NOT EXISTS sendbox (
CREATE INDEX IF NOT EXISTS idx_sendbox_address ON sendbox(address); CREATE INDEX IF NOT EXISTS idx_sendbox_address ON sendbox(address);
CREATE INDEX IF NOT EXISTS idx_sendbox_created_at ON sendbox(created_at);
CREATE TABLE IF NOT EXISTS settings ( CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
value TEXT, value TEXT,
+8 -9
View File
@@ -1,6 +1,6 @@
{ {
"name": "cloudflare_temp_email", "name": "cloudflare_temp_email",
"version": "1.0.6", "version": "1.0.5",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -10,7 +10,6 @@
"build:pages": "vite build -m pages --emptyOutDir", "build:pages": "vite build -m pages --emptyOutDir",
"build:pages:nopwa": "VITE_PWA_DISABLED=true vite build -m pages --emptyOutDir", "build:pages:nopwa": "VITE_PWA_DISABLED=true vite build -m pages --emptyOutDir",
"build:telegram": "VITE_IS_TELEGRAM=true vite build -m prod --emptyOutDir", "build:telegram": "VITE_IS_TELEGRAM=true vite build -m prod --emptyOutDir",
"build:telegram:pages": "VITE_IS_TELEGRAM=true vite build -m pages --emptyOutDir",
"build:telegram:release": "VITE_IS_TELEGRAM=true vite build -m example --emptyOutDir", "build:telegram:release": "VITE_IS_TELEGRAM=true vite build -m example --emptyOutDir",
"preview": "vite preview", "preview": "vite preview",
"deploy:telegram": "npm run build:telegram && wrangler pages deploy ./dist --branch production", "deploy:telegram": "npm run build:telegram && wrangler pages deploy ./dist --branch production",
@@ -25,15 +24,15 @@
"@vueuse/core": "^12.8.2", "@vueuse/core": "^12.8.2",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12", "@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.12.2", "axios": "^1.11.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"mail-parser-wasm": "^0.2.1", "mail-parser-wasm": "^0.2.1",
"naive-ui": "^2.43.1", "naive-ui": "^2.42.0",
"postal-mime": "^2.5.0", "postal-mime": "^2.4.4",
"vooks": "^0.2.12", "vooks": "^0.2.12",
"vue": "^3.5.22", "vue": "^3.5.20",
"vue-clipboard3": "^2.0.0", "vue-clipboard3": "^2.0.0",
"vue-i18n": "^11.1.12", "vue-i18n": "^11.1.11",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
@@ -42,13 +41,13 @@
"@vitejs/plugin-vue": "^5.2.4", "@vitejs/plugin-vue": "^5.2.4",
"unplugin-auto-import": "^19.3.0", "unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.8.0", "unplugin-vue-components": "^28.8.0",
"vite": "^6.3.6", "vite": "^6.3.5",
"vite-plugin-pwa": "^1.0.3", "vite-plugin-pwa": "^1.0.3",
"vite-plugin-top-level-await": "^1.6.0", "vite-plugin-top-level-await": "^1.6.0",
"vite-plugin-wasm": "^3.5.0", "vite-plugin-wasm": "^3.5.0",
"workbox-build": "^7.3.0", "workbox-build": "^7.3.0",
"workbox-window": "^7.3.0", "workbox-window": "^7.3.0",
"wrangler": "^4.42.2" "wrangler": "^4.33.0"
}, },
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
} }
+928 -944
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -86,7 +86,6 @@ const getOpenSettings = async (message, notification) => {
cfTurnstileSiteKey: res["cfTurnstileSiteKey"] || "", cfTurnstileSiteKey: res["cfTurnstileSiteKey"] || "",
enableWebhook: res["enableWebhook"] || false, enableWebhook: res["enableWebhook"] || false,
isS3Enabled: res["isS3Enabled"] || false, isS3Enabled: res["isS3Enabled"] || false,
enableAddressPassword: res["enableAddressPassword"] || false,
}); });
if (openSettings.value.needAuth) { if (openSettings.value.needAuth) {
showAuth.value = true; showAuth.value = true;
-3
View File
@@ -36,7 +36,6 @@ export const useGlobalState = createGlobalState(
isS3Enabled: false, isS3Enabled: false,
showGithub: true, showGithub: true,
disableAdminPasswordCheck: false, disableAdminPasswordCheck: false,
enableAddressPassword: false,
}) })
const settings = ref({ const settings = ref({
fetched: false, fetched: false,
@@ -64,7 +63,6 @@ export const useGlobalState = createGlobalState(
const auth = useStorage('auth', ''); const auth = useStorage('auth', '');
const adminAuth = useStorage('adminAuth', ''); const adminAuth = useStorage('adminAuth', '');
const jwt = useStorage('jwt', ''); const jwt = useStorage('jwt', '');
const addressPassword = useSessionStorage('addressPassword', '');
const adminTab = useSessionStorage('adminTab', "account"); const adminTab = useSessionStorage('adminTab', "account");
const adminMailTabAddress = ref(""); const adminMailTabAddress = ref("");
const adminSendBoxTabAddress = ref(""); const adminSendBoxTabAddress = ref("");
@@ -147,7 +145,6 @@ export const useGlobalState = createGlobalState(
userOauth2SessionState, userOauth2SessionState,
userOauth2SessionClientID, userOauth2SessionClientID,
useSimpleIndex, useSimpleIndex,
addressPassword,
} }
}, },
) )
-6
View File
@@ -14,7 +14,6 @@ import AccountSettings from './admin/AccountSettings.vue';
import UserManagement from './admin/UserManagement.vue'; import UserManagement from './admin/UserManagement.vue';
import UserSettings from './admin/UserSettings.vue'; import UserSettings from './admin/UserSettings.vue';
import UserOauth2Settings from './admin/UserOauth2Settings.vue'; import UserOauth2Settings from './admin/UserOauth2Settings.vue';
import RoleAddressConfig from './admin/RoleAddressConfig.vue';
import Mails from './admin/Mails.vue'; import Mails from './admin/Mails.vue';
import MailsUnknow from './admin/MailsUnknow.vue'; import MailsUnknow from './admin/MailsUnknow.vue';
import About from './common/About.vue'; import About from './common/About.vue';
@@ -62,7 +61,6 @@ const { t } = useI18n({
user_management: 'User Management', user_management: 'User Management',
user_settings: 'User Settings', user_settings: 'User Settings',
userOauth2Settings: 'Oauth2 Settings', userOauth2Settings: 'Oauth2 Settings',
roleAddressConfig: 'Role Address Config',
unknow: 'Mails with unknow receiver', unknow: 'Mails with unknow receiver',
senderAccess: 'Sender Access Control', senderAccess: 'Sender Access Control',
sendBox: 'Send Box', sendBox: 'Send Box',
@@ -90,7 +88,6 @@ const { t } = useI18n({
user_management: '用户管理', user_management: '用户管理',
user_settings: '用户设置', user_settings: '用户设置',
userOauth2Settings: 'Oauth2 设置', userOauth2Settings: 'Oauth2 设置',
roleAddressConfig: '角色地址配置',
unknow: '无收件人邮件', unknow: '无收件人邮件',
senderAccess: '发件权限控制', senderAccess: '发件权限控制',
sendBox: '发件箱', sendBox: '发件箱',
@@ -176,9 +173,6 @@ onMounted(async () => {
<n-tab-pane name="userOauth2Settings" :tab="t('userOauth2Settings')"> <n-tab-pane name="userOauth2Settings" :tab="t('userOauth2Settings')">
<UserOauth2Settings /> <UserOauth2Settings />
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="roleAddressConfig" :tab="t('roleAddressConfig')">
<RoleAddressConfig />
</n-tab-pane>
</n-tabs> </n-tabs>
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="mails" :tab="t('mails')"> <n-tab-pane name="mails" :tab="t('mails')">
+8 -308
View File
@@ -1,6 +1,6 @@
<script setup> <script setup>
import { ref, h, onMounted, watch, computed } from 'vue'; import { ref, h, onMounted, watch } from 'vue';
import { NBadge, useMessage } from 'naive-ui' import { NBadge } from 'naive-ui'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useGlobalState } from '../../store' import { useGlobalState } from '../../store'
@@ -9,7 +9,7 @@ import { NButton, NMenu } from 'naive-ui';
import { MenuFilled } from '@vicons/material' import { MenuFilled } from '@vicons/material'
const { const {
loading, adminTab, openSettings, loading, adminTab,
adminMailTabAddress, adminSendBoxTabAddress adminMailTabAddress, adminSendBoxTabAddress
} = useGlobalState() } = useGlobalState()
const message = useMessage() const message = useMessage()
@@ -33,25 +33,7 @@ const { t } = useI18n({
itemCount: 'itemCount', itemCount: 'itemCount',
query: 'Query', query: 'Query',
addressQueryTip: 'Leave blank to query all addresses', addressQueryTip: 'Leave blank to query all addresses',
clearInbox: 'Clear Inbox', actions: 'Actions'
clearSentItems: 'Clear Sent Items',
clearInboxTip: 'Are you sure to clear inbox for this email?',
clearSentItemsTip: 'Are you sure to clear sent items for this email?',
actions: 'Actions',
success: 'Success',
resetPassword: 'Reset Password',
newPassword: 'New Password',
passwordResetSuccess: 'Password reset successfully',
selectAll: 'Select All of This Page',
unselectAll: 'Unselect All',
pleaseSelectAddress: 'Please select address',
selectedItems: 'Selected',
multiDelete: 'Multi Delete',
multiDeleteTip: 'Are you sure to delete selected addresses?',
multiClearInbox: 'Multi Clear Inbox',
multiClearInboxTip: 'Are you sure to clear inbox for selected addresses?',
multiClearSentItems: 'Multi Clear Sent Items',
multiClearSentItemsTip: 'Are you sure to clear sent items for selected addresses?',
}, },
zh: { zh: {
name: '名称', name: '名称',
@@ -70,25 +52,7 @@ const { t } = useI18n({
itemCount: '总数', itemCount: '总数',
query: '查询', query: '查询',
addressQueryTip: '留空查询所有地址', addressQueryTip: '留空查询所有地址',
clearInbox: '清空收件箱',
clearSentItems: '清空发件箱',
clearInboxTip: '确定要清空这个邮箱的收件箱吗?',
clearSentItemsTip: '确定要清空这个邮箱的发件箱吗?',
actions: '操作', actions: '操作',
success: '成功',
resetPassword: '重置密码',
newPassword: '新密码',
passwordResetSuccess: '密码重置成功',
selectAll: '全选本页',
unselectAll: '取消全选',
pleaseSelectAddress: '请选择地址',
selectedItems: '已选择',
multiDelete: '批量删除',
multiDeleteTip: '确定要删除选中的邮箱吗?',
multiClearInbox: '批量清空收件箱',
multiClearInboxTip: '确定要清空选中邮箱的收件箱吗?',
multiClearSentItems: '批量清空发件箱',
multiClearSentItemsTip: '确定要清空选中邮箱的发件箱吗?',
} }
} }
}); });
@@ -96,20 +60,6 @@ const { t } = useI18n({
const showEmailCredential = ref(false) const showEmailCredential = ref(false)
const curEmailCredential = ref("") const curEmailCredential = ref("")
const curDeleteAddressId = ref(0); const curDeleteAddressId = ref(0);
const curClearInboxAddressId = ref(0);
const curClearSentItemsAddressId = ref(0);
const showResetPassword = ref(false);
const curResetPasswordAddressId = ref(0);
const newPassword = ref('');
// Multi-action mode state
const checkedRowKeys = ref([]);
const showMultiActionModal = ref(false);
const multiActionProgress = ref({ percentage: 0, tip: '0/0' });
const multiActionTitle = ref('');
const selectedCount = computed(() => checkedRowKeys.value.length);
const showMultiActionBar = computed(() => checkedRowKeys.value.length > 0);
const addressQuery = ref("") const addressQuery = ref("")
@@ -118,8 +68,6 @@ const count = ref(0)
const page = ref(1) const page = ref(1)
const pageSize = ref(20) const pageSize = ref(20)
const showDeleteAccount = ref(false) const showDeleteAccount = ref(false)
const showClearInbox = ref(false)
const showClearSentItems = ref(false)
const showCredential = async (id) => { const showCredential = async (id) => {
try { try {
@@ -135,7 +83,7 @@ const showCredential = async (id) => {
const deleteEmail = async () => { const deleteEmail = async () => {
try { try {
await api.adminDeleteAddress(curDeleteAddressId.value) await api.adminDeleteAddress(curDeleteAddressId.value)
message.success(t("success")); message.success("success");
await fetchData() await fetchData()
} catch (error) { } catch (error) {
message.error(error.message || "error"); message.error(error.message || "error");
@@ -144,142 +92,6 @@ const deleteEmail = async () => {
} }
} }
const clearInbox = async () => {
try {
await api.fetch(`/admin/clear_inbox/${curClearInboxAddressId.value}`, {
method: 'DELETE'
});
message.success(t("success"));
await fetchData()
} catch (error) {
message.error(error.message || "error");
} finally {
showClearInbox.value = false
}
}
const clearSentItems = async () => {
try {
await api.fetch(`/admin/clear_sent_items/${curClearSentItemsAddressId.value}`, {
method: 'DELETE'
});
message.success(t("success"));
await fetchData()
} catch (error) {
message.error(error.message || "error");
} finally {
showClearSentItems.value = false
}
}
const resetPassword = async () => {
try {
await api.fetch(`/admin/address/${curResetPasswordAddressId.value}/reset_password`, {
method: 'POST',
body: JSON.stringify({
password: newPassword.value
})
});
message.success(t("passwordResetSuccess"));
newPassword.value = '';
showResetPassword.value = false;
} catch (error) {
message.error(error.message || "error");
}
}
// Multi-action mode functions
const multiActionSelectAll = () => {
checkedRowKeys.value = data.value.map(item => item.id);
}
const multiActionUnselectAll = () => {
checkedRowKeys.value = [];
}
// 通用批量操作函数
const executeBatchOperation = async ({
shouldSkip = () => false,
apiCall,
title,
operationName = 'operation'
}) => {
try {
loading.value = true;
const selectedAddresses = data.value.filter((item) =>
checkedRowKeys.value.includes(item.id)
);
if (selectedAddresses.length === 0) {
message.error(t('pleaseSelectAddress'));
return;
}
const failedIds = [];
const totalCount = selectedAddresses.length;
multiActionProgress.value = {
percentage: 0,
tip: `0/${totalCount}`
};
multiActionTitle.value = title;
showMultiActionModal.value = true;
for (const [index, address] of selectedAddresses.entries()) {
try {
if (!shouldSkip(address)) {
await apiCall(address.id);
}
} catch (error) {
console.error(`${operationName} failed for address ${address.id}:`, error);
failedIds.push(address.id);
}
multiActionProgress.value = {
percentage: Math.floor((index + 1) / totalCount * 100),
tip: `${index + 1}/${totalCount}`
};
}
await fetchData();
checkedRowKeys.value = failedIds;
message.success(t("success"));
} catch (error) {
message.error(error.message || "error");
} finally {
loading.value = false;
}
}
const multiActionDeleteAccounts = async () => {
await executeBatchOperation({
apiCall: (id) => api.adminDeleteAddress(id),
title: t('multiDelete') + ' ' + t('success'),
operationName: 'Delete'
});
}
const multiActionClearInbox = async () => {
await executeBatchOperation({
shouldSkip: (address) => address.mail_count <= 0,
apiCall: (id) => api.fetch(`/admin/clear_inbox/${id}`, {
method: 'DELETE'
}),
title: t('multiClearInbox') + ' ' + t('success'),
operationName: 'ClearInbox'
});
}
const multiActionClearSentItems = async () => {
await executeBatchOperation({
shouldSkip: (address) => address.send_count <= 0,
apiCall: (id) => api.fetch(`/admin/clear_sent_items/${id}`, {
method: 'DELETE'
}),
title: t('multiClearSentItems') + ' ' + t('success'),
operationName: 'ClearSentItems'
});
}
const fetchData = async () => { const fetchData = async () => {
try { try {
addressQuery.value = addressQuery.value.trim() addressQuery.value = addressQuery.value.trim()
@@ -294,15 +106,12 @@ const fetchData = async () => {
count.value = addressCount; count.value = addressCount;
} }
} catch (error) { } catch (error) {
console.error(error); console.log(error)
message.error(error.message || "error"); message.error(error.message || "error");
} }
} }
const columns = [ const columns = [
{
type: 'selection'
},
{ {
title: "ID", title: "ID",
key: "id" key: "id"
@@ -419,45 +228,6 @@ const columns = [
), ),
show: row.send_count > 0 show: row.send_count > 0
}, },
{
label: () => h(NButton,
{
text: true,
onClick: () => {
curClearInboxAddressId.value = row.id;
showClearInbox.value = true;
}
},
{ default: () => t('clearInbox') }
),
show: row.mail_count > 0
},
{
label: () => h(NButton,
{
text: true,
onClick: () => {
curClearSentItemsAddressId.value = row.id;
showClearSentItems.value = true;
}
},
{ default: () => t('clearSentItems') }
),
show: row.send_count > 0
},
{
label: () => h(NButton,
{
text: true,
onClick: () => {
curResetPasswordAddressId.value = row.id;
showResetPassword.value = true;
}
},
{ default: () => t('resetPassword') }
),
show: openSettings.value?.enableAddressPassword
},
{ {
label: () => h(NButton, label: () => h(NButton,
{ {
@@ -511,70 +281,13 @@ onMounted(async () => {
</n-button> </n-button>
</template> </template>
</n-modal> </n-modal>
<n-modal v-model:show="showClearInbox" preset="dialog" :title="t('clearInbox')"> <n-input-group>
<p>{{ t('clearInboxTip') }}</p>
<template #action>
<n-button :loading="loading" @click="clearInbox" size="small" tertiary type="error">
{{ t('clearInbox') }}
</n-button>
</template>
</n-modal>
<n-modal v-model:show="showClearSentItems" preset="dialog" :title="t('clearSentItems')">
<p>{{ t('clearSentItemsTip') }}</p>
<template #action>
<n-button :loading="loading" @click="clearSentItems" size="small" tertiary type="error">
{{ t('clearSentItems') }}
</n-button>
</template>
</n-modal>
<n-modal v-model:show="showResetPassword" preset="dialog" :title="t('resetPassword')">
<n-form-item :label="t('newPassword')">
<n-input v-model:value="newPassword" type="password" placeholder="" show-password-on="click" />
</n-form-item>
<template #action>
<n-button :loading="loading" @click="resetPassword" size="small" tertiary type="info">
{{ t('resetPassword') }}
</n-button>
</template>
</n-modal>
<n-input-group style="margin-bottom: 10px;">
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')" <n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')"
@keydown.enter="fetchData" /> @keydown.enter="fetchData" />
<n-button @click="fetchData" type="primary" tertiary> <n-button @click="fetchData" type="primary" tertiary>
{{ t('query') }} {{ t('query') }}
</n-button> </n-button>
</n-input-group> </n-input-group>
<n-space v-if="showMultiActionBar" style="margin-bottom: 10px;">
<n-button @click="multiActionSelectAll" tertiary>
{{ t('selectAll') }}
</n-button>
<n-button @click="multiActionUnselectAll" tertiary>
{{ t('unselectAll') }}
</n-button>
<n-popconfirm @positive-click="multiActionDeleteAccounts">
<template #trigger>
<n-button tertiary type="error">{{ t('multiDelete') }}</n-button>
</template>
{{ t('multiDeleteTip') }}
</n-popconfirm>
<n-popconfirm @positive-click="multiActionClearInbox">
<template #trigger>
<n-button tertiary type="warning">{{ t('multiClearInbox') }}</n-button>
</template>
{{ t('multiClearInboxTip') }}
</n-popconfirm>
<n-popconfirm @positive-click="multiActionClearSentItems">
<template #trigger>
<n-button tertiary type="warning">{{ t('multiClearSentItems') }}</n-button>
</template>
{{ t('multiClearSentItemsTip') }}
</n-popconfirm>
<n-tag type="info">
{{ t('selectedItems') }}: {{ selectedCount }}
</n-tag>
</n-space>
<div style="overflow: auto;"> <div style="overflow: auto;">
<div style="display: inline-block;"> <div style="display: inline-block;">
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" <n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
@@ -584,21 +297,8 @@ onMounted(async () => {
</template> </template>
</n-pagination> </n-pagination>
</div> </div>
<n-data-table v-model:checked-row-keys="checkedRowKeys" :columns="columns" :data="data" :bordered="false" <n-data-table :columns="columns" :data="data" :bordered="false" embedded />
:row-key="row => row.id" embedded />
</div> </div>
<!-- Multi-action progress modal -->
<n-modal v-model:show="showMultiActionModal" preset="dialog" :title="multiActionTitle" negative-text="OK">
<n-space justify="center">
<n-progress type="circle" status="info" :percentage="multiActionProgress.percentage">
<span style="text-align: center">
{{ multiActionProgress.tip }}
</span>
</n-progress>
</n-space>
</n-modal>
</div> </div>
</template> </template>
+6 -38
View File
@@ -13,7 +13,6 @@ const { t } = useI18n({
messages: { messages: {
en: { en: {
tip: 'You can manually input the following multiple select input and enter', tip: 'You can manually input the following multiple select input and enter',
manualInputPrompt: 'Type and press Enter to add',
save: 'Save', save: 'Save',
successTip: 'Save Success', successTip: 'Save Success',
address_block_list: 'Address Block Keywords for Users(Admin can skip)', address_block_list: 'Address Block Keywords for Users(Admin can skip)',
@@ -39,7 +38,6 @@ const { t } = useI18n({
}, },
zh: { zh: {
tip: '您可以手动输入以下多选输入框, 回车增加', tip: '您可以手动输入以下多选输入框, 回车增加',
manualInputPrompt: '输入后按回车键添加',
save: '保存', save: '保存',
successTip: '保存成功', successTip: '保存成功',
address_block_list: '邮件地址屏蔽关键词(管理员可跳过检查)', address_block_list: '邮件地址屏蔽关键词(管理员可跳过检查)',
@@ -211,55 +209,25 @@ onMounted(async () => {
</n-flex> </n-flex>
<n-form-item-row :label="t('address_block_list')"> <n-form-item-row :label="t('address_block_list')">
<n-select v-model:value="addressBlockList" filterable multiple tag <n-select v-model:value="addressBlockList" filterable multiple tag
:placeholder="t('address_block_list_placeholder')"> :placeholder="t('address_block_list_placeholder')" />
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-form-item-row> </n-form-item-row>
<n-form-item-row :label="t('send_address_block_list')"> <n-form-item-row :label="t('send_address_block_list')">
<n-select v-model:value="sendAddressBlockList" filterable multiple tag <n-select v-model:value="sendAddressBlockList" filterable multiple tag
:placeholder="t('address_block_list_placeholder')"> :placeholder="t('address_block_list_placeholder')" />
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-form-item-row> </n-form-item-row>
<n-form-item-row :label="t('noLimitSendAddressList')"> <n-form-item-row :label="t('noLimitSendAddressList')">
<n-select v-model:value="noLimitSendAddressList" filterable multiple tag <n-select v-model:value="noLimitSendAddressList" filterable multiple tag
:placeholder="t('noLimitSendAddressList')"> :placeholder="t('noLimitSendAddressList')" />
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-form-item-row> </n-form-item-row>
<n-form-item-row :label="t('verified_address_list')"> <n-form-item-row :label="t('verified_address_list')">
<n-select v-model:value="verifiedAddressList" filterable multiple tag <n-select v-model:value="verifiedAddressList" filterable multiple tag
:placeholder="t('verified_address_list')"> :placeholder="t('verified_address_list')" />
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-form-item-row> </n-form-item-row>
<n-form-item-row :label="t('fromBlockList')"> <n-form-item-row :label="t('fromBlockList')">
<n-select v-model:value="fromBlockList" filterable multiple tag :placeholder="t('fromBlockList')"> <n-select v-model:value="fromBlockList" filterable multiple tag :placeholder="t('fromBlockList')" />
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-form-item-row> </n-form-item-row>
<n-form-item-row :label="t('block_receive_unknow_address_email')"> <n-form-item-row :label="t('block_receive_unknow_address_email')">
<n-switch v-model:value="emailRuleSettings.blockReceiveUnknowAddressEmail" :round="false" /> <n-checkbox v-model:checked="emailRuleSettings.blockReceiveUnknowAddressEmail" />
</n-form-item-row> </n-form-item-row>
<n-form-item-row :label="t('email_forwarding_config')"> <n-form-item-row :label="t('email_forwarding_config')">
<n-button @click="openEmailForwardingModal">{{ t('config') }}</n-button> <n-button @click="openEmailForwardingModal">{{ t('config') }}</n-button>
+4 -33
View File
@@ -15,13 +15,10 @@ const { t } = useI18n({
en: { en: {
address: 'Address', address: 'Address',
enablePrefix: 'If enable Prefix', enablePrefix: 'If enable Prefix',
creatNewEmail: 'Create New Email', creatNewEmail: 'Get New Email',
fillInAllFields: 'Please fill in all fields', fillInAllFields: 'Please fill in all fields',
successTip: 'Success Created', successTip: 'Success Created',
addressCredential: 'Mail Address Credential', addressCredential: 'Mail Address Credential',
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',
}, },
zh: { zh: {
address: '地址', address: '地址',
@@ -30,9 +27,6 @@ const { t } = useI18n({
fillInAllFields: '请填写完整信息', fillInAllFields: '请填写完整信息',
successTip: '创建成功', successTip: '创建成功',
addressCredential: '邮箱地址凭证', addressCredential: '邮箱地址凭证',
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
addressPassword: '地址密码',
linkWithAddressCredential: '打开即可自动登录邮箱的链接',
} }
} }
}); });
@@ -42,8 +36,6 @@ const emailName = ref("")
const emailDomain = ref("") const emailDomain = ref("")
const showReultModal = ref(false) const showReultModal = ref(false)
const result = ref("") const result = ref("")
const addressPassword = ref("")
const createdAddress = ref("")
const newEmail = async () => { const newEmail = async () => {
if (!emailName.value || !emailDomain.value) { if (!emailName.value || !emailDomain.value) {
@@ -60,8 +52,6 @@ const newEmail = async () => {
}) })
}) })
result.value = res["jwt"]; result.value = res["jwt"];
addressPassword.value = res["password"] || '';
createdAddress.value = res["address"] || '';
message.success(t('successTip')) message.success(t('successTip'))
showReultModal.value = true showReultModal.value = true
} catch (error) { } catch (error) {
@@ -69,10 +59,6 @@ const newEmail = async () => {
} }
} }
const getUrlWithJwt = () => {
return `${window.location.origin}/?jwt=${result.value}`
}
onMounted(async () => { onMounted(async () => {
if (openSettings.prefix) { if (openSettings.prefix) {
enablePrefix.value = true enablePrefix.value = true
@@ -84,29 +70,14 @@ onMounted(async () => {
<template> <template>
<div class="center"> <div class="center">
<n-modal v-model:show="showReultModal" preset="dialog" :title="t('addressCredential')"> <n-modal v-model:show="showReultModal" preset="dialog" :title="t('addressCredential')">
<span> <p>{{ t('addressCredential') }}</p>
<p>{{ t("addressCredentialTip") }}</p> <n-card :bordered="false" embedded>
</span>
<n-card embedded>
<b>{{ result }}</b> <b>{{ result }}</b>
</n-card> </n-card>
<n-card embedded v-if="addressPassword">
<p><b>{{ createdAddress }}</b></p>
<p>{{ t('addressPassword') }}: <b>{{ addressPassword }}</b></p>
</n-card>
<n-card embedded>
<n-collapse>
<n-collapse-item :title='t("linkWithAddressCredential")'>
<n-card embedded>
<b>{{ getUrlWithJwt() }}</b>
</n-card>
</n-collapse-item>
</n-collapse>
</n-card>
</n-modal> </n-modal>
<n-card :bordered="false" embedded style="max-width: 600px;"> <n-card :bordered="false" embedded style="max-width: 600px;">
<n-form-item-row v-if="openSettings.prefix" :label="t('enablePrefix')"> <n-form-item-row v-if="openSettings.prefix" :label="t('enablePrefix')">
<n-switch v-model:value="enablePrefix" :round="false" /> <n-checkbox v-model:checked="enablePrefix" />
</n-form-item-row> </n-form-item-row>
<n-form-item-row :label="t('address')"> <n-form-item-row :label="t('address')">
<n-input-group> <n-input-group>
-16
View File
@@ -17,8 +17,6 @@ const cleanupModel = ref({
cleanAddressDays: 30, cleanAddressDays: 30,
enableInactiveAddressAutoCleanup: false, enableInactiveAddressAutoCleanup: false,
cleanInactiveAddressDays: 30, cleanInactiveAddressDays: 30,
enableUnboundAddressAutoCleanup: false,
cleanUnboundAddressDays: 30,
}) })
const { t } = useI18n({ const { t } = useI18n({
@@ -30,7 +28,6 @@ const { t } = useI18n({
sendBoxLabel: "Cleanup the sendbox before n days", sendBoxLabel: "Cleanup the sendbox before n days",
addressCreateLabel: "Cleanup the address created before n days", addressCreateLabel: "Cleanup the address created before n days",
inactiveAddressLabel: "Cleanup the inactive address before n days", inactiveAddressLabel: "Cleanup the inactive address before n days",
unboundAddressLabel: "Cleanup the unbound address before n days",
cleanupNow: "Cleanup now", cleanupNow: "Cleanup now",
autoCleanup: "Auto cleanup", autoCleanup: "Auto cleanup",
cleanupSuccess: "Cleanup success", cleanupSuccess: "Cleanup success",
@@ -44,7 +41,6 @@ const { t } = useI18n({
sendBoxLabel: "清理 n 天前的发件箱", sendBoxLabel: "清理 n 天前的发件箱",
addressCreateLabel: "清理 n 天前创建的地址", addressCreateLabel: "清理 n 天前创建的地址",
inactiveAddressLabel: "清理 n 天前的未活跃地址", inactiveAddressLabel: "清理 n 天前的未活跃地址",
unboundAddressLabel: "清理 n 天前的未绑定用户地址",
autoCleanup: "自动清理", autoCleanup: "自动清理",
cleanupSuccess: "清理成功", cleanupSuccess: "清理成功",
cleanupNow: "立即清理", cleanupNow: "立即清理",
@@ -165,18 +161,6 @@ onMounted(async () => {
{{ t('cleanupNow') }} {{ t('cleanupNow') }}
</n-button> </n-button>
</n-form-item-row> </n-form-item-row>
<n-form-item-row :label="t('unboundAddressLabel')">
<n-checkbox v-model:checked="cleanupModel.enableUnboundAddressAutoCleanup">
{{ t('autoCleanup') }}
</n-checkbox>
<n-input-number v-model:value="cleanupModel.cleanUnboundAddressDays" :placeholder="t('tip')" />
<n-button @click="cleanup('unboundAddress', cleanupModel.cleanUnboundAddressDays)">
<template #icon>
<n-icon :component="CleaningServicesFilled" />
</template>
{{ t('cleanupNow') }}
</n-button>
</n-form-item-row>
</n-form> </n-form>
</n-card> </n-card>
</div> </div>
@@ -1,153 +0,0 @@
<script setup>
import { ref, onMounted, h } from 'vue';
import { useI18n } from 'vue-i18n'
import { NInputNumber, NTag, NSpace, NButton } from 'naive-ui';
import { useGlobalState } from '../../store'
import { api } from '../../api'
const { loading } = useGlobalState()
const message = useMessage()
const { t } = useI18n({
messages: {
en: {
role: 'Role',
maxAddressCount: 'Max Address Count',
save: 'Save',
successTip: 'Success',
noRolesAvailable: 'No roles available in system config',
roleConfigDesc: 'Configure maximum address count for each user role. Role-based limits take priority over global settings.',
notConfigured: 'Not Configured (Use Global Settings)',
},
zh: {
role: '角色',
maxAddressCount: '最大地址数量',
save: '保存',
successTip: '成功',
noRolesAvailable: '系统配置中没有可用的角色',
roleConfigDesc: '为每个用户角色配置最大地址数量。角色配置优先于全局设置。',
notConfigured: '未配置(使用全局设置)',
}
}
});
const systemRoles = ref([])
const tableData = ref([])
const fetchUserRoles = async () => {
try {
const results = await api.fetch(`/admin/user_roles`);
systemRoles.value = results;
} catch (error) {
console.log(error)
message.error(error.message || "error");
}
}
const fetchRoleConfigs = async () => {
try {
const { configs } = await api.fetch(`/admin/role_address_config`);
tableData.value = systemRoles.value.map(roleObj => ({
role: roleObj.role,
max_address_count: configs[roleObj.role]?.maxAddressCount ?? null,
}));
} catch (error) {
console.log(error)
message.error(error.message || "error");
}
}
const saveConfig = async () => {
try {
// convert tableData to object with nested structure
const configs = {};
tableData.value.forEach(row => {
if (row.max_address_count !== null && row.max_address_count !== undefined) {
configs[row.role] = { maxAddressCount: row.max_address_count };
}
});
await api.fetch(`/admin/role_address_config`, {
method: 'POST',
body: JSON.stringify({ configs })
});
message.success(t('successTip'));
await fetchRoleConfigs();
} catch (error) {
console.log(error)
message.error(error.message || "error");
}
}
const columns = [
{
title: t('role'),
key: 'role',
width: 200,
render(row) {
return h(NTag, {
type: 'info',
bordered: false
}, {
default: () => row.role
})
}
},
{
title: t('maxAddressCount'),
key: 'max_address_count',
render(row) {
return h(NInputNumber, {
value: row.max_address_count,
min: 0,
max: 999,
clearable: true,
placeholder: t('notConfigured'),
style: 'width: 200px;',
onUpdateValue: (value) => {
row.max_address_count = value;
}
})
}
}
]
onMounted(async () => {
await fetchUserRoles();
await fetchRoleConfigs();
})
</script>
<template>
<div style="margin-top: 10px;">
<n-alert type="info" :bordered="false" style="margin-bottom: 20px;">
{{ t('roleConfigDesc') }}
</n-alert>
<n-alert v-if="systemRoles.length === 0" type="warning" :bordered="false">
{{ t('noRolesAvailable') }}
</n-alert>
<div v-else>
<n-space justify="end" style="margin-bottom: 12px;">
<n-button :loading="loading" @click="saveConfig" type="primary">
{{ t('save') }}
</n-button>
</n-space>
<n-data-table
:columns="columns"
:data="tableData"
:bordered="false"
embedded
/>
</div>
</div>
</template>
<style scoped>
.n-data-table {
min-width: 600px;
}
</style>
+23 -44
View File
@@ -15,29 +15,25 @@ const { t } = useI18n({
init: 'Init', init: 'Init',
successTip: 'Success', successTip: 'Success',
status: 'Check Status', status: 'Check Status',
enableTelegramAllowList: 'Enable Telegram Allow List(Manually input Chat ID)', enableTelegramAllowList: 'Enable Telegram Allow List(Manually input user ID)',
enable: 'Enable', enable: 'Enable',
telegramAllowList: 'Telegram Allow List(Manually input telegram Chat ID)', telegramAllowList: 'Telegram Allow List(Manually input telegram user ID)',
manualInputPrompt: 'Type and press Enter to add',
save: 'Save', save: 'Save',
miniAppUrl: 'Telegram Mini App URL', miniAppUrl: 'Telegram Mini App URL',
enableGlobalMailPush: 'Enable Global Mail Push(Manually input telegram Chat ID)', enableGlobalMailPush: 'Enable Global Mail Push(Manually input telegram user ID)',
globalMailPushList: 'Global Mail Push Chat ID List', globalMailPushList: 'Global Mail Push List',
globalMailPushListTip: 'Support chat_id of private chat/group/channel. You can send a message to your bot, then visit this link to see chat_id, https://api.telegram.org/bot<Replace with your BOT TOKEN>/getUpdates',
}, },
zh: { zh: {
init: '初始化', init: '初始化',
successTip: '成功', successTip: '成功',
status: '查看状态', status: '查看状态',
enableTelegramAllowList: '启用 Telegram 白名单(手动输入 Chat ID, 回车增加)', enableTelegramAllowList: '启用 Telegram 白名单(手动输入用户 ID, 回车增加)',
enable: '启用', enable: '启用',
telegramAllowList: 'Telegram 白名单(手动输入 Chat ID, 回车增加)', telegramAllowList: 'Telegram 白名单(手动输入用户 ID, 回车增加)',
manualInputPrompt: '输入后按回车键添加',
save: '保存', save: '保存',
miniAppUrl: '电报小程序 URL(请输入你部署的电报小程序网页地址)', miniAppUrl: '电报小程序 URL(请输入你部署的电报小程序网页地址)',
enableGlobalMailPush: '启用全局邮件推送(手动输入邮箱管理员的 telegram Chat ID, 回车增加)', enableGlobalMailPush: '启用全局邮件推送(手动输入邮箱管理员的 telegram 用户 ID, 回车增加)',
globalMailPushList: '全局邮件推送 Chat ID 列表', globalMailPushList: '全局邮件推送用户列表',
globalMailPushListTip: '支持对话/群组/频道的 Chat ID, 您可以发送一条消息给您的机器人,然后访问此链接来查看 chat_id, https://api.telegram.org/bot<这里替换成您的 BOT TOKEN>/getUpdates',
} }
} }
}); });
@@ -117,17 +113,6 @@ onMounted(async () => {
<template> <template>
<div class="center"> <div class="center">
<n-card :bordered="false" embedded style="max-width: 800px; overflow: auto;"> <n-card :bordered="false" embedded style="max-width: 800px; overflow: auto;">
<n-flex justify="end">
<n-button @click="fetchStatus" secondary>
{{ t('status') }}
</n-button>
<n-button @click="init" type="primary">
{{ t('init') }}
</n-button>
<n-button @click="saveSettings" type="primary">
{{ t('save') }}
</n-button>
</n-flex>
<n-card :bordered="false" embedded> <n-card :bordered="false" embedded>
<n-form-item-row :label="t('enableTelegramAllowList')"> <n-form-item-row :label="t('enableTelegramAllowList')">
<n-input-group> <n-input-group>
@@ -135,41 +120,31 @@ onMounted(async () => {
{{ t('enable') }} {{ t('enable') }}
</n-checkbox> </n-checkbox>
<n-select v-model:value="settings.allowList" filterable multiple tag style="width: 80%;" <n-select v-model:value="settings.allowList" filterable multiple tag style="width: 80%;"
:placeholder="t('telegramAllowList')"> :placeholder="t('telegramAllowList')" />
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-input-group> </n-input-group>
</n-form-item-row> </n-form-item-row>
<br />
<n-form-item-row :label="t('enableGlobalMailPush')"> <n-form-item-row :label="t('enableGlobalMailPush')">
<n-input-group> <n-input-group>
<n-checkbox v-model:checked="settings.enableGlobalMailPush" style="width: 20%;"> <n-checkbox v-model:checked="settings.enableGlobalMailPush" style="width: 20%;">
{{ t('enable') }} {{ t('enable') }}
</n-checkbox> </n-checkbox>
<n-select v-model:value="settings.globalMailPushList" filterable multiple tag <n-select v-model:value="settings.globalMailPushList" filterable multiple tag
style="width: 80%;" :placeholder="t('globalMailPushList')"> style="width: 80%;" :placeholder="t('globalMailPushList')" />
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-input-group> </n-input-group>
<template #feedback>
<n-text depth="3">
{{ t('globalMailPushListTip') }}
</n-text>
</template>
</n-form-item-row> </n-form-item-row>
<br />
<n-form-item-row :label="t('miniAppUrl')"> <n-form-item-row :label="t('miniAppUrl')">
<n-input v-model:value="settings.miniAppUrl"></n-input> <n-input v-model:value="settings.miniAppUrl"></n-input>
</n-form-item-row> </n-form-item-row>
<n-button @click="saveSettings" type="primary" block>
{{ t('save') }}
</n-button>
</n-card> </n-card>
<n-button @click="init" type="primary" block>
{{ t('init') }}
</n-button>
<n-button @click="fetchStatus" secondary block>
{{ t('status') }}
</n-button>
<pre v-if="status.fetched">{{ JSON.stringify(status, null, 2) }}</pre> <pre v-if="status.fetched">{{ JSON.stringify(status, null, 2) }}</pre>
</n-card> </n-card>
</div> </div>
@@ -182,4 +157,8 @@ onMounted(async () => {
place-items: center; place-items: center;
justify-content: center; justify-content: center;
} }
.n-button {
margin-top: 10px;
}
</style> </style>
@@ -21,7 +21,6 @@ const { t } = useI18n({
successTip: 'Save Success', successTip: 'Save Success',
enable: 'Enable', enable: 'Enable',
enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)', enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)',
manualInputPrompt: 'Type and press Enter to add',
mailAllowList: 'Mail Address Allow List', mailAllowList: 'Mail Address Allow List',
addOauth2: 'Add Oauth2', addOauth2: 'Add Oauth2',
name: 'Name', name: 'Name',
@@ -34,7 +33,6 @@ const { t } = useI18n({
successTip: '保存成功', successTip: '保存成功',
enable: '启用', enable: '启用',
enableMailAllowList: '启用邮件地址白名单(可手动输入, 回车增加)', enableMailAllowList: '启用邮件地址白名单(可手动输入, 回车增加)',
manualInputPrompt: '输入后按回车键添加',
mailAllowList: '邮件地址白名单', mailAllowList: '邮件地址白名单',
addOauth2: '添加 Oauth2', addOauth2: '添加 Oauth2',
name: '名称', name: '名称',
@@ -186,7 +184,7 @@ onMounted(async () => {
</template> </template>
</n-modal> </n-modal>
<n-card :bordered="false" embedded style="max-width: 600px;"> <n-card :bordered="false" embedded style="max-width: 600px;">
<n-alert :show-icon="false" :bordered="false" type="warning" closable style="margin-bottom: 10px;"> <n-alert :show-icon="false" type="warning" closable style="margin-bottom: 10px;">
{{ t("tip") }} {{ t("tip") }}
</n-alert> </n-alert>
<n-flex justify="end"> <n-flex justify="end">
@@ -248,13 +246,7 @@ onMounted(async () => {
</n-checkbox> </n-checkbox>
<n-select v-model:value="item.mailAllowList" v-if="item.enableMailAllowList" filterable <n-select v-model:value="item.mailAllowList" v-if="item.enableMailAllowList" filterable
multiple tag style="width: 80%;" :options="mailAllowOptions" multiple tag style="width: 80%;" :options="mailAllowOptions"
:placeholder="t('mailAllowList')"> :placeholder="t('mailAllowList')" />
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-input-group> </n-input-group>
</n-form-item-row> </n-form-item-row>
</n-form> </n-form>
+5 -15
View File
@@ -18,7 +18,6 @@ const { t } = useI18n({
enableMailVerify: 'Enable Mail Verify (Send address must be an address in the system with a balance and can send mail normally)', enableMailVerify: 'Enable Mail Verify (Send address must be an address in the system with a balance and can send mail normally)',
verifyMailSender: 'Verify Mail Sender', verifyMailSender: 'Verify Mail Sender',
enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)', enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)',
manualInputPrompt: 'Type and press Enter to add',
mailAllowList: 'Mail Address Allow List', mailAllowList: 'Mail Address Allow List',
maxAddressCount: 'Maximum number of email addresses that can be binded', maxAddressCount: 'Maximum number of email addresses that can be binded',
}, },
@@ -30,7 +29,6 @@ const { t } = useI18n({
enableMailVerify: '启用邮件验证(发送地址必须是系统中能有余额且能正常发送邮件的地址)', enableMailVerify: '启用邮件验证(发送地址必须是系统中能有余额且能正常发送邮件的地址)',
verifyMailSender: '验证邮件发送地址', verifyMailSender: '验证邮件发送地址',
enableMailAllowList: '启用邮件地址白名单(可手动输入, 回车增加)', enableMailAllowList: '启用邮件地址白名单(可手动输入, 回车增加)',
manualInputPrompt: '输入后按回车键添加',
mailAllowList: '邮件地址白名单', mailAllowList: '邮件地址白名单',
maxAddressCount: '可绑定最大邮箱地址数量', maxAddressCount: '可绑定最大邮箱地址数量',
} }
@@ -85,14 +83,9 @@ onMounted(async () => {
<template> <template>
<div class="center"> <div class="center">
<n-card :bordered="false" embedded style="max-width: 600px;"> <n-card :bordered="false" embedded style="max-width: 600px;">
<n-flex justify="end">
<n-button @click="save" type="primary" :loading="loading">
{{ t('save') }}
</n-button>
</n-flex>
<n-form :model="userSettings"> <n-form :model="userSettings">
<n-form-item-row :label="t('enableUserRegister')"> <n-form-item-row :label="t('enableUserRegister')">
<n-switch v-model:value="userSettings.enable" :round="false" /> <n-checkbox v-model:checked="userSettings.enable" />
</n-form-item-row> </n-form-item-row>
<n-form-item-row :label="t('enableMailVerify')"> <n-form-item-row :label="t('enableMailVerify')">
<n-input-group> <n-input-group>
@@ -110,13 +103,7 @@ onMounted(async () => {
</n-checkbox> </n-checkbox>
<n-select v-model:value="userSettings.mailAllowList" v-if="userSettings.enableMailAllowList" <n-select v-model:value="userSettings.mailAllowList" v-if="userSettings.enableMailAllowList"
filterable multiple tag style="width: 80%;" :options="mailAllowOptions" filterable multiple tag style="width: 80%;" :options="mailAllowOptions"
:placeholder="t('mailAllowList')"> :placeholder="t('mailAllowList')" />
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-input-group> </n-input-group>
</n-form-item-row> </n-form-item-row>
<n-form-item-row :label="t('maxAddressCount')"> <n-form-item-row :label="t('maxAddressCount')">
@@ -125,6 +112,9 @@ onMounted(async () => {
:placeholder="t('maxAddressCount')" /> :placeholder="t('maxAddressCount')" />
</n-input-group> </n-input-group>
</n-form-item-row> </n-form-item-row>
<n-button @click="save" type="primary" block :loading="loading">
{{ t('save') }}
</n-button>
</n-form> </n-form>
</n-card> </n-card>
</div> </div>
+8 -25
View File
@@ -13,17 +13,13 @@ const { t } = useI18n({
messages: { messages: {
en: { en: {
successTip: 'Success', successTip: 'Success',
enableAllowList: 'Enable Allow List (Restrict webhook access to specific users)', webhookAllowList: 'Webhook Allow List(Enter the address that is allowed to use webhook and enter)',
webhookAllowList: 'Webhook Allow List(Enter the mail address that is allowed to use webhook and enter)',
manualInputPrompt: 'Type and press Enter to add',
save: 'Save', save: 'Save',
notEnabled: 'Webhook is not enabled', notEnabled: 'Webhook is not enabled',
}, },
zh: { zh: {
successTip: '成功', successTip: '成功',
enableAllowList: '启用白名单 (限制 webhook 访问权限,只有白名单中的用户可以使用)', webhookAllowList: 'Webhook 白名单(请输入允许使用webhook 的地址, 回车增加)',
webhookAllowList: 'Webhook 白名单(请输入允许使用webhook 的邮箱地址, 回车增加)',
manualInputPrompt: '输入后按回车键添加',
save: '保存', save: '保存',
notEnabled: 'Webhook 未开启', notEnabled: 'Webhook 未开启',
} }
@@ -31,16 +27,14 @@ const { t } = useI18n({
}); });
class WebhookSettings { class WebhookSettings {
enableAllowList: boolean;
allowList: string[]; allowList: string[];
constructor(enableAllowList: boolean, allowList: string[]) { constructor(allowList: string[]) {
this.enableAllowList = enableAllowList;
this.allowList = allowList; this.allowList = allowList;
} }
} }
const webhookSettings = ref(new WebhookSettings(false, [])) const webhookSettings = ref(new WebhookSettings([]))
const webhookEnabled = ref(false) const webhookEnabled = ref(false)
const errorInfo = ref('') const errorInfo = ref('')
@@ -74,24 +68,13 @@ onMounted(async () => {
<template> <template>
<div class="center"> <div class="center">
<n-card v-if="webhookEnabled" :bordered="false" embedded style="max-width: 800px; overflow: auto;"> <n-card v-if="webhookEnabled" :bordered="false" embedded style="max-width: 800px; overflow: auto;">
<n-flex justify="end">
<n-button @click="saveSettings" type="primary">
{{ t('save') }}
</n-button>
</n-flex>
<n-form-item-row :label="t('enableAllowList')">
<n-switch v-model:value="webhookSettings.enableAllowList" :round="false" />
</n-form-item-row>
<n-form-item-row :label="t('webhookAllowList')"> <n-form-item-row :label="t('webhookAllowList')">
<n-select v-model:value="webhookSettings.allowList" filterable multiple tag <n-select v-model:value="webhookSettings.allowList" filterable multiple tag
:placeholder="t('webhookAllowList')"> :placeholder="t('webhookAllowList')" />
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-form-item-row> </n-form-item-row>
<n-button @click="saveSettings" type="primary" block>
{{ t('save') }}
</n-button>
</n-card> </n-card>
<n-result v-else status="404" :title="t('notEnabled')" :description="errorInfo" /> <n-result v-else status="404" :title="t('notEnabled')" :description="errorInfo" />
</div> </div>
+1 -1
View File
@@ -26,7 +26,7 @@ onMounted(async () => {
<template> <template>
<div class="center"> <div class="center">
<n-card :bordered="false" embedded style="max-width: 800px; overflow: auto;"> <n-card :bordered="false" embedded style="max-width: 600px; overflow: auto;">
<pre>{{ JSON.stringify(settings, null, 2) }}</pre> <pre>{{ JSON.stringify(settings, null, 2) }}</pre>
</n-card> </n-card>
</div> </div>
+6 -85
View File
@@ -9,7 +9,7 @@ import Turnstile from '../../components/Turnstile.vue'
import { useGlobalState } from '../../store' import { useGlobalState } from '../../store'
import { api } from '../../api' import { api } from '../../api'
import { getRouterPathWithLang, hashPassword } from '../../utils' import { getRouterPathWithLang } from '../../utils'
const props = defineProps({ const props = defineProps({
bindUserAddress: { bindUserAddress: {
@@ -39,7 +39,7 @@ const router = useRouter()
const { const {
jwt, loading, openSettings, jwt, loading, openSettings,
showAddressCredential, userSettings, addressPassword showAddressCredential, userSettings
} = useGlobalState() } = useGlobalState()
const tabValue = ref('signin') const tabValue = ref('signin')
@@ -47,47 +47,8 @@ const credential = ref('')
const emailName = ref("") const emailName = ref("")
const emailDomain = ref("") const emailDomain = ref("")
const cfToken = ref("") const cfToken = ref("")
const loginMethod = ref('credential') // 'credential' or 'password'
const loginAddress = ref('')
const loginPassword = ref('')
// 根据 openSettings 初始化登录方式
const initLoginMethod = () => {
if (openSettings.value?.enableAddressPassword) {
loginMethod.value = 'password';
} else {
loginMethod.value = 'credential';
}
}
const login = async () => { const login = async () => {
if (loginMethod.value === 'password') {
// Password login
if (!loginAddress.value || !loginPassword.value) {
message.error(t('emailPasswordRequired'));
return;
}
try {
const res = await api.fetch('/api/address_login', {
method: 'POST',
body: JSON.stringify({
email: loginAddress.value,
password: await hashPassword(loginPassword.value)
})
});
jwt.value = res.jwt;
await api.getSettings();
try {
await props.bindUserAddress();
} catch (error) {
message.error(`${t('bindUserAddressError')}: ${error.message}`);
}
await router.push(getRouterPathWithLang("/", locale.value));
} catch (error) {
message.error(error.message || "error");
}
return;
}
if (!credential.value) { if (!credential.value) {
message.error(t('credentialInput')); message.error(t('credentialInput'));
return; return;
@@ -124,11 +85,6 @@ const { locale, t } = useI18n({
bindUserInfo: 'Logged in user, login without binding email or create new email address will bind to current user', 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', bindUserAddressError: 'Error when bind email address to user',
autoGeneratedName: 'Auto-generated name', autoGeneratedName: 'Auto-generated name',
passwordLogin: 'Password Login',
credentialLogin: 'Credential Login',
email: 'Email',
password: 'Password',
emailPasswordRequired: 'Email and password are required',
}, },
zh: { zh: {
login: '登录', login: '登录',
@@ -146,11 +102,6 @@ const { locale, t } = useI18n({
bindUserInfo: '已登录用户, 登录未绑定邮箱或创建新邮箱地址将绑定到当前用户', bindUserInfo: '已登录用户, 登录未绑定邮箱或创建新邮箱地址将绑定到当前用户',
bindUserAddressError: '绑定邮箱地址到用户时错误', bindUserAddressError: '绑定邮箱地址到用户时错误',
autoGeneratedName: '自动生成名称', autoGeneratedName: '自动生成名称',
passwordLogin: '密码登录',
credentialLogin: '凭据登录',
email: '邮箱',
password: '密码',
emailPasswordRequired: '邮箱和密码不能为空',
} }
} }
}); });
@@ -206,7 +157,6 @@ const newEmail = async () => {
cfToken.value cfToken.value
); );
jwt.value = res["jwt"]; jwt.value = res["jwt"];
addressPassword.value = res["password"] || '';
await api.getSettings(); await api.getSettings();
await router.push(getRouterPathWithLang("/", locale.value)); await router.push(getRouterPathWithLang("/", locale.value));
showAddressCredential.value = true; showAddressCredential.value = true;
@@ -262,7 +212,6 @@ onMounted(async () => {
await api.getOpenSettings(message, notification); await api.getOpenSettings(message, notification);
} }
emailDomain.value = domainsOptions.value ? domainsOptions.value[0]?.value : ""; emailDomain.value = domainsOptions.value ? domainsOptions.value[0]?.value : "";
initLoginMethod();
}); });
</script> </script>
@@ -274,29 +223,9 @@ onMounted(async () => {
<n-tabs v-if="openSettings.fetched" v-model:value="tabValue" size="large" justify-content="space-evenly"> <n-tabs v-if="openSettings.fetched" v-model:value="tabValue" size="large" justify-content="space-evenly">
<n-tab-pane name="signin" :tab="loginAndBindTag"> <n-tab-pane name="signin" :tab="loginAndBindTag">
<n-form> <n-form>
<div v-if="loginMethod === 'password'">
<n-form-item-row :label="t('email')" required>
<n-input v-model:value="loginAddress" />
</n-form-item-row>
<n-form-item-row :label="t('password')" required>
<n-input v-model:value="loginPassword" type="password" show-password-on="click" />
</n-form-item-row>
</div>
<div v-else>
<n-form-item-row :label="t('credential')" required> <n-form-item-row :label="t('credential')" required>
<n-input v-model:value="credential" type="textarea" :autosize="{ minRows: 3 }" /> <n-input v-model:value="credential" type="textarea" :autosize="{ minRows: 3 }" />
</n-form-item-row> </n-form-item-row>
</div>
<div class="switch-login-button">
<n-button v-if="openSettings?.enableAddressPassword"
@click="loginMethod === 'password' ? loginMethod = 'credential' : loginMethod = 'password'"
type="info" quaternary size="tiny">
{{ loginMethod === 'password' ? t('credentialLogin') : t('passwordLogin') }}
</n-button>
</div>
<n-button @click="login" :loading="loading" type="primary" block secondary strong> <n-button @click="login" :loading="loading" type="primary" block secondary strong>
<template #icon> <template #icon>
<n-icon :component="EmailOutlined" /> <n-icon :component="EmailOutlined" />
@@ -315,21 +244,19 @@ onMounted(async () => {
<n-spin :show="generateNameLoading"> <n-spin :show="generateNameLoading">
<n-form> <n-form>
<span> <span>
<p v-if="!openSettings.disableCustomAddressName">{{ t("getNewEmailTip1") + <p v-if="!openSettings.disableCustomAddressName">{{ t("getNewEmailTip1") + addressRegex.source }}</p>
addressRegex.source }}</p>
<p v-if="!openSettings.disableCustomAddressName">{{ t("getNewEmailTip2") }}</p> <p v-if="!openSettings.disableCustomAddressName">{{ t("getNewEmailTip2") }}</p>
<p>{{ t("getNewEmailTip3") }}</p> <p>{{ t("getNewEmailTip3") }}</p>
</span> </span>
<n-button v-if="!openSettings.disableCustomAddressName" @click="generateName" <n-button v-if="!openSettings.disableCustomAddressName" @click="generateName" style="margin-bottom: 10px;">
style="margin-bottom: 10px;">
{{ t('generateName') }} {{ t('generateName') }}
</n-button> </n-button>
<n-input-group> <n-input-group>
<n-input-group-label v-if="addressPrefix"> <n-input-group-label v-if="addressPrefix">
{{ addressPrefix }} {{ addressPrefix }}
</n-input-group-label> </n-input-group-label>
<n-input v-if="!openSettings.disableCustomAddressName" v-model:value="emailName" show-count <n-input v-if="!openSettings.disableCustomAddressName" v-model:value="emailName" show-count :minlength="openSettings.minAddressLen"
:minlength="openSettings.minAddressLen" :maxlength="openSettings.maxAddressLen" /> :maxlength="openSettings.maxAddressLen" />
<n-input v-else :value="t('autoGeneratedName')" disabled /> <n-input v-else :value="t('autoGeneratedName')" disabled />
<n-input-group-label>@</n-input-group-label> <n-input-group-label>@</n-input-group-label>
<n-select v-model:value="emailDomain" :consistent-menu-width="false" <n-select v-model:value="emailDomain" :consistent-menu-width="false"
@@ -367,12 +294,6 @@ onMounted(async () => {
margin-top: 10px; margin-top: 10px;
} }
.switch-login-button {
display: flex;
justify-content: center;
margin: 10px 0;
}
.n-form { .n-form {
text-align: left; text-align: left;
} }
+2 -119
View File
@@ -5,22 +5,16 @@ import { useRouter } from 'vue-router'
import { useGlobalState } from '../../store' import { useGlobalState } from '../../store'
import { api } from '../../api' import { api } from '../../api'
import { hashPassword } from '../../utils'
import { getRouterPathWithLang } from '../../utils' import { getRouterPathWithLang } from '../../utils'
const { const {
jwt, settings, showAddressCredential, loading, openSettings jwt, settings, showAddressCredential, loading
} = useGlobalState() } = useGlobalState()
const router = useRouter() const router = useRouter()
const message = useMessage() const message = useMessage()
const showLogout = ref(false) const showLogout = ref(false)
const showDeleteAccount = ref(false) const showDeleteAccount = ref(false)
const showClearInbox = ref(false)
const showClearSentItems = ref(false)
const showChangePassword = ref(false)
const newPassword = ref('')
const confirmPassword = ref('')
const { locale, t } = useI18n({ const { locale, t } = useI18n({
messages: { messages: {
en: { en: {
@@ -30,16 +24,6 @@ const { locale, t } = useI18n({
logoutConfirm: 'Are you sure to logout?', logoutConfirm: 'Are you sure to logout?',
deleteAccount: "Delete Account", deleteAccount: "Delete Account",
deleteAccountConfirm: "Are you sure to delete your account and all emails for this account?", deleteAccountConfirm: "Are you sure to delete your account and all emails for this account?",
clearInbox: "Clear Inbox",
clearSentItems: "Clear Sent Items",
clearInboxConfirm: "Are you sure to clear all emails in your inbox?",
clearSentItemsConfirm: "Are you sure to clear all emails in your sent items?",
success: "Success",
changePassword: "Change Password",
newPassword: "New Password",
confirmPassword: "Confirm Password",
passwordMismatch: "Passwords do not match",
passwordChanged: "Password changed successfully",
}, },
zh: { zh: {
logout: '退出登录', logout: '退出登录',
@@ -48,16 +32,6 @@ const { locale, t } = useI18n({
logoutConfirm: '确定要退出登录吗?', logoutConfirm: '确定要退出登录吗?',
deleteAccount: "删除账户", deleteAccount: "删除账户",
deleteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?", deleteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?",
clearInbox: "清空收件箱",
clearSentItems: "清空发件箱",
clearInboxConfirm: "确定要清空你收件箱中的所有邮件吗?",
clearSentItemsConfirm: "确定要清空你发件箱中的所有邮件吗?",
success: "成功",
changePassword: "修改密码",
newPassword: "新密码",
confirmPassword: "确认密码",
passwordMismatch: "密码不匹配",
passwordChanged: "密码修改成功",
} }
} }
}); });
@@ -80,53 +54,6 @@ const deleteAccount = async () => {
message.error(error.message || "error"); message.error(error.message || "error");
} }
}; };
const clearInbox = async () => {
try {
await api.fetch(`/api/clear_inbox`, {
method: 'DELETE'
});
message.success(t("success"));
} catch (error) {
message.error(error.message || "error");
} finally {
showClearInbox.value = false;
}
};
const clearSentItems = async () => {
try {
await api.fetch(`/api/clear_sent_items`, {
method: 'DELETE'
});
message.success(t("success"));
} catch (error) {
message.error(error.message || "error");
} finally {
showClearSentItems.value = false;
}
};
const changePassword = async () => {
if (newPassword.value !== confirmPassword.value) {
message.error(t("passwordMismatch"));
return;
}
try {
await api.fetch(`/api/address_change_password`, {
method: 'POST',
body: JSON.stringify({
new_password: await hashPassword(newPassword.value)
})
});
message.success(t("passwordChanged"));
newPassword.value = '';
confirmPassword.value = '';
showChangePassword.value = false;
} catch (error) {
message.error(error.message || "error");
}
};
</script> </script>
<template> <template>
@@ -135,22 +62,10 @@ const changePassword = async () => {
<n-button @click="showAddressCredential = true" type="primary" secondary block strong> <n-button @click="showAddressCredential = true" type="primary" secondary block strong>
{{ t('showAddressCredential') }} {{ t('showAddressCredential') }}
</n-button> </n-button>
<n-button v-if="openSettings?.enableAddressPassword" @click="showChangePassword = true" type="info" secondary block strong>
{{ t('changePassword') }}
</n-button>
<n-button v-if="openSettings.enableUserDeleteEmail" @click="showClearInbox = true" type="warning" secondary
block strong>
{{ t('clearInbox') }}
</n-button>
<n-button v-if="openSettings.enableUserDeleteEmail" @click="showClearSentItems = true" type="warning"
secondary block strong>
{{ t('clearSentItems') }}
</n-button>
<n-button @click="showLogout = true" secondary block strong> <n-button @click="showLogout = true" secondary block strong>
{{ t('logout') }} {{ t('logout') }}
</n-button> </n-button>
<n-button v-if="openSettings.enableUserDeleteEmail" @click="showDeleteAccount = true" type="error" secondary <n-button @click="showDeleteAccount = true" type="error" secondary block strong>
block strong>
{{ t('deleteAccount') }} {{ t('deleteAccount') }}
</n-button> </n-button>
</n-card> </n-card>
@@ -170,38 +85,6 @@ const changePassword = async () => {
</n-button> </n-button>
</template> </template>
</n-modal> </n-modal>
<n-modal v-model:show="showClearInbox" preset="dialog" :title="t('clearInbox')">
<p>{{ t('clearInboxConfirm') }}</p>
<template #action>
<n-button :loading="loading" @click="clearInbox" size="small" tertiary type="warning">
{{ t('clearInbox') }}
</n-button>
</template>
</n-modal>
<n-modal v-model:show="showClearSentItems" preset="dialog" :title="t('clearSentItems')">
<p>{{ t('clearSentItemsConfirm') }}</p>
<template #action>
<n-button :loading="loading" @click="clearSentItems" size="small" tertiary type="warning">
{{ t('clearSentItems') }}
</n-button>
</template>
</n-modal>
<n-modal v-model:show="showChangePassword" preset="dialog" :title="t('changePassword')">
<n-form :model="{ newPassword, confirmPassword }">
<n-form-item :label="t('newPassword')">
<n-input v-model:value="newPassword" type="password" placeholder="" show-password-on="click" />
</n-form-item>
<n-form-item :label="t('confirmPassword')">
<n-input v-model:value="confirmPassword" type="password" placeholder="" show-password-on="click" />
</n-form-item>
</n-form>
<template #action>
<n-button :loading="loading" @click="changePassword" size="small" tertiary type="info">
{{ t('changePassword') }}
</n-button>
</template>
</n-modal>
</div> </div>
</template> </template>
+1 -7
View File
@@ -19,7 +19,7 @@ const router = useRouter()
const { const {
jwt, settings, showAddressCredential, userJwt, jwt, settings, showAddressCredential, userJwt,
isTelegram, openSettings, addressPassword isTelegram, openSettings
} = useGlobalState() } = useGlobalState()
const { locale, t } = useI18n({ const { locale, t } = useI18n({
@@ -34,7 +34,6 @@ const { locale, t } = useI18n({
addressCredential: 'Mail Address Credential', addressCredential: 'Mail Address Credential',
linkWithAddressCredential: 'Open to auto login email link', linkWithAddressCredential: 'Open to auto login email link',
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',
userLogin: 'User Login', userLogin: 'User Login',
}, },
zh: { zh: {
@@ -47,7 +46,6 @@ const { locale, t } = useI18n({
addressCredential: '邮箱地址凭证', addressCredential: '邮箱地址凭证',
linkWithAddressCredential: '打开即可自动登录邮箱的链接', linkWithAddressCredential: '打开即可自动登录邮箱的链接',
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。', addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
addressPassword: '地址密码',
userLogin: '用户登录', userLogin: '用户登录',
} }
} }
@@ -151,10 +149,6 @@ onMounted(async () => {
<n-card embedded> <n-card embedded>
<b>{{ jwt }}</b> <b>{{ jwt }}</b>
</n-card> </n-card>
<n-card embedded v-if="addressPassword">
<p><b>{{ settings.address }}</b></p>
<p>{{ t('addressPassword') }}: <b>{{ addressPassword }}</b></p>
</n-card>
<n-card embedded> <n-card embedded>
<n-collapse> <n-collapse>
<n-collapse-item :title='t("linkWithAddressCredential")'> <n-collapse-item :title='t("linkWithAddressCredential")'>
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "temp-email-pages", "name": "temp-email-pages",
"version": "1.0.6", "version": "1.0.5",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -11,7 +11,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"wrangler": "^4.42.2" "wrangler": "^4.33.0"
}, },
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
} }
@@ -35,7 +35,6 @@
| `DOMAIN_LABELS` | JSON | 对于中文域名,可以使用 DOMAIN_LABELS 显示域名的中文展示名称 | `["中文.awsl.uk", "dreamhunter2333.xyz"]` | | `DOMAIN_LABELS` | JSON | 对于中文域名,可以使用 DOMAIN_LABELS 显示域名的中文展示名称 | `["中文.awsl.uk", "dreamhunter2333.xyz"]` |
| `ENABLE_AUTO_REPLY` | 文本/JSON | 允许自动回复邮件 | `true` | | `ENABLE_AUTO_REPLY` | 文本/JSON | 允许自动回复邮件 | `true` |
| `DEFAULT_SEND_BALANCE` | 文本/JSON | 默认发送邮件余额,如果不设置,将为 0 | `1` | | `DEFAULT_SEND_BALANCE` | 文本/JSON | 默认发送邮件余额,如果不设置,将为 0 | `1` |
| `ENABLE_ADDRESS_PASSWORD` | 文本/JSON | 启用邮箱地址密码功能,启用后创建新地址时会自动生成密码,并支持密码登录和修改 | `true` |
## 接受邮件相关变量 ## 接受邮件相关变量
+3 -3
View File
@@ -1,12 +1,12 @@
{ {
"name": "temp-mail-docs", "name": "temp-mail-docs",
"private": true, "private": true,
"version": "1.0.6", "version": "1.0.5",
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@types/node": "^24.7.2", "@types/node": "^24.3.0",
"vitepress": "^1.6.4", "vitepress": "^1.6.4",
"wrangler": "^4.42.2" "wrangler": "^4.33.0"
}, },
"scripts": { "scripts": {
"dev": "vitepress dev docs", "dev": "vitepress dev docs",
+392 -410
View File
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -1,6 +1,6 @@
{ {
"name": "cloudflare_temp_email", "name": "cloudflare_temp_email",
"version": "1.0.6", "version": "1.0.5",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -11,23 +11,23 @@
"build": "wrangler deploy --dry-run --outdir dist --minify" "build": "wrangler deploy --dry-run --outdir dist --minify"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20251011.0", "@cloudflare/workers-types": "^4.20250826.0",
"@eslint/js": "9.18.0", "@eslint/js": "9.18.0",
"@simplewebauthn/types": "10.0.0", "@simplewebauthn/types": "10.0.0",
"@types/node": "^22.18.10", "@types/node": "^22.18.0",
"eslint": "9.18.0", "eslint": "9.18.0",
"globals": "^15.15.0", "globals": "^15.15.0",
"typescript-eslint": "^8.46.0", "typescript-eslint": "^8.41.0",
"wrangler": "^4.42.2" "wrangler": "^4.33.0"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.888.0", "@aws-sdk/client-s3": "^3.876.0",
"@aws-sdk/s3-request-presigner": "3.888.0", "@aws-sdk/s3-request-presigner": "^3.876.0",
"@simplewebauthn/server": "10.0.1", "@simplewebauthn/server": "10.0.1",
"hono": "^4.9.11", "hono": "^4.9.4",
"jsonpath-plus": "^10.3.0", "jsonpath-plus": "^10.3.0",
"mimetext": "^3.0.27", "mimetext": "^3.0.27",
"postal-mime": "^2.5.0", "postal-mime": "^2.4.4",
"resend": "^4.8.0", "resend": "^4.8.0",
"telegraf": "4.16.3", "telegraf": "4.16.3",
"worker-mailer": "^1.1.5" "worker-mailer": "^1.1.5"
+891 -897
View File
File diff suppressed because it is too large Load Diff
+1 -11
View File
@@ -2,7 +2,7 @@ import { Context } from 'hono';
import { CONSTANTS } from '../constants'; import { CONSTANTS } from '../constants';
import { getJsonSetting, saveSetting, checkUserPassword, getDomains, getUserRoles } from '../utils'; import { getJsonSetting, saveSetting, checkUserPassword, getDomains, getUserRoles } from '../utils';
import { UserSettings, GeoData, UserInfo, RoleAddressConfig } from "../models"; import { UserSettings, GeoData, UserInfo } from "../models";
import { handleListQuery } from '../common' import { handleListQuery } from '../common'
import UserBindAddressModule from '../user_api/bind_address'; import UserBindAddressModule from '../user_api/bind_address';
import i18n from '../i18n'; import i18n from '../i18n';
@@ -166,14 +166,4 @@ export default {
results: results, results: results,
}); });
}, },
getRoleAddressConfig: async (c: Context<HonoCustomType>) => {
const value = await getJsonSetting<RoleAddressConfig>(c, CONSTANTS.ROLE_ADDRESS_CONFIG_KEY);
const configs = value || {};
return c.json({ configs });
},
saveRoleAddressConfig: async (c: Context<HonoCustomType>) => {
const { configs } = await c.req.json<{ configs: RoleAddressConfig }>();
await saveSetting(c, CONSTANTS.ROLE_ADDRESS_CONFIG_KEY, JSON.stringify(configs));
return c.json({ success: true });
},
} }
+1 -20
View File
@@ -14,22 +14,15 @@ CREATE TABLE IF NOT EXISTS raw_mails (
CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address); CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address);
CREATE INDEX IF NOT EXISTS idx_raw_mails_created_at ON raw_mails(created_at);
CREATE TABLE IF NOT EXISTS address ( CREATE TABLE IF NOT EXISTS address (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE, name TEXT UNIQUE,
password TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX IF NOT EXISTS idx_address_name ON address(name); CREATE INDEX IF NOT EXISTS idx_address_name ON address(name);
CREATE INDEX IF NOT EXISTS idx_address_created_at ON address(created_at);
CREATE INDEX IF NOT EXISTS idx_address_updated_at ON address(updated_at);
CREATE TABLE IF NOT EXISTS auto_reply_mails ( CREATE TABLE IF NOT EXISTS auto_reply_mails (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
source_prefix TEXT, source_prefix TEXT,
@@ -61,7 +54,6 @@ CREATE TABLE IF NOT EXISTS sendbox (
); );
CREATE INDEX IF NOT EXISTS idx_sendbox_address ON sendbox(address); CREATE INDEX IF NOT EXISTS idx_sendbox_address ON sendbox(address);
CREATE INDEX IF NOT EXISTS idx_sendbox_created_at ON sendbox(created_at);
CREATE TABLE IF NOT EXISTS settings ( CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
@@ -137,20 +129,9 @@ export default {
}, },
migrate: async (c: Context<HonoCustomType>) => { migrate: async (c: Context<HonoCustomType>) => {
const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY); const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY);
if (version == "v0.0.2") {
// example migration from v0.0.2 to v0.0.3
const query = `ALTER TABLE address ADD password TEXT;`
await c.env.DB.exec(query);
}
if (version != CONSTANTS.DB_VERSION) { if (version != CONSTANTS.DB_VERSION) {
// TODO: Perform migration logic here // TODO: Perform migration logic here
// remove all \r and \n characters from the query string
// split by ; and join with a ;\n
const query = DB_INIT_QUERIES.replace(/[\r\n]/g, "")
.split(";")
.map((query) => query.trim())
.join(";\n");
await c.env.DB.exec(query);
// Update the version in the settings table // Update the version in the settings table
await utils.saveSetting(c, CONSTANTS.DB_VERSION_KEY, CONSTANTS.DB_VERSION); await utils.saveSetting(c, CONSTANTS.DB_VERSION_KEY, CONSTANTS.DB_VERSION);
return c.json({ return c.json({
+1 -56
View File
@@ -2,7 +2,7 @@ import { Hono } from 'hono'
import { Jwt } from 'hono/utils/jwt' import { Jwt } from 'hono/utils/jwt'
import i18n from '../i18n' import i18n from '../i18n'
import { sendAdminInternalMail, getJsonSetting, saveSetting, getUserRoles, getBooleanValue, hashPassword } from '../utils' import { sendAdminInternalMail, getJsonSetting, saveSetting, getUserRoles } from '../utils'
import { newAddress, handleListQuery } from '../common' import { newAddress, handleListQuery } from '../common'
import { CONSTANTS } from '../constants' import { CONSTANTS } from '../constants'
import cleanup_api from './cleanup_api' import cleanup_api from './cleanup_api'
@@ -56,7 +56,6 @@ api.post('/admin/new_address', async (c) => {
checkAllowDomains: false, checkAllowDomains: false,
enableCheckNameRegex: false, enableCheckNameRegex: false,
}); });
return c.json(res); return c.json(res);
} catch (e) { } catch (e) {
return c.text(`${msgs.FailedCreateAddressMsg}: ${(e as Error).message}`, 400) return c.text(`${msgs.FailedCreateAddressMsg}: ${(e as Error).message}`, 400)
@@ -90,34 +89,6 @@ api.delete('/admin/delete_address/:id', async (c) => {
}) })
}) })
api.delete('/admin/clear_inbox/:id', async (c) => {
const { id } = c.req.param();
const { success: mailSuccess } = await c.env.DB.prepare(
`DELETE FROM raw_mails WHERE address IN`
+ ` (select name from address where id = ?) `
).bind(id).run();
if (!mailSuccess) {
return c.text("Failed to clear inbox", 500)
}
return c.json({
success: mailSuccess
})
})
api.delete('/admin/clear_sent_items/:id', async (c) => {
const { id } = c.req.param();
const { success: sendboxSuccess } = await c.env.DB.prepare(
`DELETE FROM sendbox WHERE address IN`
+ ` (select name from address where id = ?) `
).bind(id).run();
if (!sendboxSuccess) {
return c.text("Failed to clear sent items", 500)
}
return c.json({
success: sendboxSuccess
})
})
api.get('/admin/show_password/:id', async (c) => { api.get('/admin/show_password/:id', async (c) => {
const { id } = c.req.param(); const { id } = c.req.param();
const name = await c.env.DB.prepare( const name = await c.env.DB.prepare(
@@ -132,30 +103,6 @@ api.get('/admin/show_password/:id', async (c) => {
}) })
}) })
api.post('/admin/address/:id/reset_password', async (c) => {
const { id } = c.req.param();
const { password } = await c.req.json();
// 检查功能是否启用
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
return c.text("Password management is disabled", 403);
}
if (!password) {
return c.text("Password is required", 400);
}
const hashedPassword = await hashPassword(password);
const { success } = await c.env.DB.prepare(
`UPDATE address SET password = ?, updated_at = datetime('now') WHERE id = ?`
).bind(hashedPassword, id).run();
if (!success) {
return c.text("Failed to reset password", 500);
}
return c.json({ success: true });
})
// mail api // mail api
api.get('/admin/mails', admin_mail_api.getMails); api.get('/admin/mails', admin_mail_api.getMails);
api.get('/admin/mails_unknow', admin_mail_api.getUnknowMails); api.get('/admin/mails_unknow', admin_mail_api.getUnknowMails);
@@ -344,8 +291,6 @@ api.post('/admin/users', admin_user_api.createUser)
api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword) api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword)
api.get('/admin/user_roles', async (c) => c.json(getUserRoles(c))) api.get('/admin/user_roles', async (c) => c.json(getUserRoles(c)))
api.post('/admin/user_roles', admin_user_api.updateUserRoles) api.post('/admin/user_roles', admin_user_api.updateUserRoles)
api.get('/admin/role_address_config', admin_user_api.getRoleAddressConfig)
api.post('/admin/role_address_config', admin_user_api.saveRoleAddressConfig)
api.get('/admin/users/bind_address/:user_id', admin_user_api.getBindedAddresses) api.get('/admin/users/bind_address/:user_id', admin_user_api.getBindedAddresses)
api.post('/admin/users/bind_address', admin_user_api.bindAddress) api.post('/admin/users/bind_address', admin_user_api.bindAddress)
+1 -1
View File
@@ -4,7 +4,7 @@ import { AdminWebhookSettings } from "../models";
async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response> { async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
const settings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json"); const settings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
return c.json(settings || new AdminWebhookSettings(false, [])); return c.json(settings || new AdminWebhookSettings([]));
} }
async function saveWebhookSettings(c: Context<HonoCustomType>): Promise<Response> { async function saveWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
+1 -2
View File
@@ -40,8 +40,7 @@ api.get('/open_api/settings', async (c) => {
"isS3Enabled": isS3Enabled(c), "isS3Enabled": isS3Enabled(c),
"version": CONSTANTS.VERSION, "version": CONSTANTS.VERSION,
"showGithub": !utils.getBooleanValue(c.env.DISABLE_SHOW_GITHUB), "showGithub": !utils.getBooleanValue(c.env.DISABLE_SHOW_GITHUB),
"disableAdminPasswordCheck": utils.getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK), "disableAdminPasswordCheck": utils.getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK)
"enableAddressPassword": utils.getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)
}); });
}) })
+6 -46
View File
@@ -1,7 +1,7 @@
import { Context } from 'hono'; import { Context } from 'hono';
import { Jwt } from 'hono/utils/jwt' import { Jwt } from 'hono/utils/jwt'
import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList, hashPassword } from './utils'; import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList } 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';
@@ -82,37 +82,6 @@ export async function updateAddressUpdatedAt(
} }
} }
export const generateRandomPassword = (): string => {
const charset = "abcdefghijklmnopqrstuvwxyz0123456789";
let password = "";
for (let i = 0; i < 8; i++) {
password += charset.charAt(Math.floor(Math.random() * charset.length));
}
return password;
}
const generatePasswordForAddress = async (
c: Context<HonoCustomType>,
address: string
): Promise<string | null> => {
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
return null;
}
const plainPassword = generateRandomPassword();
const hashedPassword = await hashPassword(plainPassword);
const { success } = await c.env.DB.prepare(
`UPDATE address SET password = ?, updated_at = datetime('now') WHERE name = ?`
).bind(hashedPassword, address).run();
if (!success) {
console.warn("Failed to set generated password for address:", address);
return null;
}
return plainPassword;
}
export const newAddress = async ( export const newAddress = async (
c: Context<HonoCustomType>, c: Context<HonoCustomType>,
{ {
@@ -131,7 +100,7 @@ export const newAddress = async (
checkAllowDomains?: boolean, checkAllowDomains?: boolean,
enableCheckNameRegex?: boolean, enableCheckNameRegex?: boolean,
} }
): Promise<{ address: string, jwt: string, password?: string | null }> => { ): Promise<{ address: string, jwt: string }> => {
// trim whitespace and remove special characters // trim whitespace and remove special characters
name = name.trim().replace(getNameRegex(c), '') name = name.trim().replace(getNameRegex(c), '')
// check name // check name
@@ -197,10 +166,6 @@ export const newAddress = async (
const address_id = await c.env.DB.prepare( const address_id = await c.env.DB.prepare(
`SELECT id FROM address where name = ?` `SELECT id FROM address where name = ?`
).bind(name).first<number>("id"); ).bind(name).first<number>("id");
// 如果启用地址密码功能,自动生成密码
const generatedPassword = await generatePasswordForAddress(c, name);
// create jwt // create jwt
const jwt = await Jwt.sign({ const jwt = await Jwt.sign({
address: name, address: name,
@@ -209,7 +174,6 @@ export const newAddress = async (
return { return {
jwt: jwt, jwt: jwt,
address: name, address: name,
password: generatedPassword,
} }
} }
@@ -251,12 +215,6 @@ export const cleanup = async (
`created_at < datetime('now', '-${cleanDays} day')` `created_at < datetime('now', '-${cleanDays} day')`
) )
break; break;
case "unboundAddress":
await batchDeleteAddressWithData(
c,
`id NOT IN (SELECT address_id FROM users_address) AND created_at < datetime('now', '-${cleanDays} day')`
)
break;
case "mails": case "mails":
await c.env.DB.prepare(` await c.env.DB.prepare(`
DELETE FROM raw_mails WHERE created_at < datetime('now', '-${cleanDays} day')` DELETE FROM raw_mails WHERE created_at < datetime('now', '-${cleanDays} day')`
@@ -310,7 +268,9 @@ const batchDeleteAddressWithData = async (
return true; return true;
} }
/**
* TODO: need senbox delete?
*/
export const deleteAddressWithData = async ( export const deleteAddressWithData = async (
c: Context<HonoCustomType>, c: Context<HonoCustomType>,
address: string | undefined | null, address: string | undefined | null,
@@ -520,7 +480,7 @@ export async function triggerWebhook(
// user mail webhook // user mail webhook
const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json"); const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
if (!adminSettings?.enableAllowList || adminSettings?.allowList.includes(address)) { if (adminSettings?.allowList.includes(address)) {
const settings = await c.env.KV.get<WebhookSettings>( const settings = await c.env.KV.get<WebhookSettings>(
`${CONSTANTS.WEBHOOK_KV_USER_SETTINGS_KEY}:${address}`, "json" `${CONSTANTS.WEBHOOK_KV_USER_SETTINGS_KEY}:${address}`, "json"
); );
+2 -3
View File
@@ -1,9 +1,9 @@
export const CONSTANTS = { export const CONSTANTS = {
VERSION: 'v' + '1.0.6', VERSION: 'v' + '1.0.5',
// DB Version // DB Version
DB_VERSION_KEY: 'db_version', DB_VERSION_KEY: 'db_version',
DB_VERSION: "v0.0.3", DB_VERSION: "v0.0.1",
// DB settings // DB settings
ADDRESS_BLOCK_LIST_KEY: 'address_block_list', ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
@@ -14,7 +14,6 @@ export const CONSTANTS = {
VERIFIED_ADDRESS_LIST_KEY: 'verified_address_list', VERIFIED_ADDRESS_LIST_KEY: 'verified_address_list',
NO_LIMIT_SEND_ADDRESS_LIST_KEY: 'no_limit_send_address_list', NO_LIMIT_SEND_ADDRESS_LIST_KEY: 'no_limit_send_address_list',
EMAIL_RULE_SETTINGS_KEY: 'email_rule_settings', EMAIL_RULE_SETTINGS_KEY: 'email_rule_settings',
ROLE_ADDRESS_CONFIG_KEY: 'role_address_config',
// KV // KV
TG_KV_PREFIX: "temp-mail-telegram", TG_KV_PREFIX: "temp-mail-telegram",
-8
View File
@@ -38,14 +38,6 @@ const messages: LocaleMessages = {
Oauth2FailedGetUserInfoMsg: "Failed to get user info from Oauth2 provider", Oauth2FailedGetUserInfoMsg: "Failed to get user info from Oauth2 provider",
Oauth2FailedGetAccessTokenMsg: "Failed to get access token from Oauth2 provider", Oauth2FailedGetAccessTokenMsg: "Failed to get access token from Oauth2 provider",
Oauth2FailedGetUserEmailMsg: "Failed to get user email from Oauth2 provider", Oauth2FailedGetUserEmailMsg: "Failed to get user email from Oauth2 provider",
PasswordChangeDisabledMsg: "Password change is disabled",
NewPasswordRequiredMsg: "New password is required",
InvalidAddressTokenMsg: "Invalid address token",
FailedUpdatePasswordMsg: "Failed to update password",
PasswordLoginDisabledMsg: "Password login is disabled",
EmailPasswordRequiredMsg: "Email and password are required",
AddressNotFoundMsg: "Address not found",
} }
export default messages; export default messages;
-8
View File
@@ -36,12 +36,4 @@ export type LocaleMessages = {
Oauth2FailedGetUserInfoMsg: string Oauth2FailedGetUserInfoMsg: string
Oauth2FailedGetAccessTokenMsg: string Oauth2FailedGetAccessTokenMsg: string
Oauth2FailedGetUserEmailMsg: string Oauth2FailedGetUserEmailMsg: string
PasswordChangeDisabledMsg: string
NewPasswordRequiredMsg: string
InvalidAddressTokenMsg: string
FailedUpdatePasswordMsg: string
PasswordLoginDisabledMsg: string
EmailPasswordRequiredMsg: string
AddressNotFoundMsg: string
} }
-8
View File
@@ -38,14 +38,6 @@ const messages: LocaleMessages = {
Oauth2FailedGetUserInfoMsg: "从 Oauth2 提供商获取用户信息失败", Oauth2FailedGetUserInfoMsg: "从 Oauth2 提供商获取用户信息失败",
Oauth2FailedGetAccessTokenMsg: "从 Oauth2 提供商获取访问令牌失败", Oauth2FailedGetAccessTokenMsg: "从 Oauth2 提供商获取访问令牌失败",
Oauth2FailedGetUserEmailMsg: "从 Oauth2 提供商获取用户邮箱失败", Oauth2FailedGetUserEmailMsg: "从 Oauth2 提供商获取用户邮箱失败",
PasswordChangeDisabledMsg: "密码修改已禁用",
NewPasswordRequiredMsg: "新密码不能为空",
InvalidAddressTokenMsg: "无效的地址令牌",
FailedUpdatePasswordMsg: "更新密码失败",
PasswordLoginDisabledMsg: "密码登录已禁用",
EmailPasswordRequiredMsg: "邮箱和密码不能为空",
AddressNotFoundMsg: "邮箱地址不存在",
} }
export default messages; export default messages;
-79
View File
@@ -1,79 +0,0 @@
import { Context } from 'hono';
import i18n from '../i18n';
import { getBooleanValue, hashPassword } from '../utils';
import { Jwt } from 'hono/utils/jwt';
export default {
// 修改地址密码
changePassword: async (c: Context<HonoCustomType>) => {
const { new_password } = await c.req.json();
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
const { address, address_id } = c.get("jwtPayload");
// 检查功能是否启用
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
return c.text(msgs.PasswordChangeDisabledMsg, 403);
}
if (!new_password) {
return c.text(msgs.NewPasswordRequiredMsg, 400);
}
if (!address || !address_id) {
return c.text(msgs.InvalidAddressTokenMsg, 400);
}
// 更新密码
const { success } = await c.env.DB.prepare(
`UPDATE address SET password = ?, updated_at = datetime('now') WHERE id = ?`
).bind(new_password, address_id).run();
if (!success) {
return c.text(msgs.FailedUpdatePasswordMsg, 500);
}
return c.json({ success: true });
},
// 地址密码登录
login: async (c: Context<HonoCustomType>) => {
const { email, password, cf_token } = await c.req.json();
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
// 检查功能是否启用
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
return c.text(msgs.PasswordLoginDisabledMsg, 403);
}
if (!email || !password) {
return c.text(msgs.EmailPasswordRequiredMsg, 400);
}
// 查找地址
const address = await c.env.DB.prepare(
`SELECT * FROM address WHERE name = ?`
).bind(email).first();
if (!address) {
return c.text(msgs.AddressNotFoundMsg, 404);
}
// 验证密码
if (address.password !== password) {
return c.text(msgs.InvalidEmailOrPasswordMsg, 401);
}
// 创建JWT
const jwt = await Jwt.sign({
address: address.name,
address_id: address.id
}, c.env.JWT_SECRET, "HS256");
return c.json({
jwt: jwt,
address: address.name
});
}
};
-40
View File
@@ -7,7 +7,6 @@ import { CONSTANTS } from '../constants'
import auto_reply from './auto_reply' import auto_reply from './auto_reply'
import webhook_settings from './webhook_settings'; import webhook_settings from './webhook_settings';
import s3_attachment from './s3_attachment'; import s3_attachment from './s3_attachment';
import address_auth from './address_auth';
export const api = new Hono<HonoCustomType>() export const api = new Hono<HonoCustomType>()
@@ -163,42 +162,3 @@ api.delete('/api/delete_address', async (c) => {
success: success success: success
}) })
}) })
api.delete('/api/clear_inbox', async (c) => {
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
}
const { address } = c.get("jwtPayload")
const { success } = await c.env.DB.prepare(
`DELETE FROM raw_mails WHERE address = ?`
).bind(address).run();
if (!success) {
return c.text("Failed to clear inbox", 500)
}
return c.json({
success: success
})
})
api.delete('/api/clear_sent_items', async (c) => {
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
}
const { address } = c.get("jwtPayload")
const { success } = await c.env.DB.prepare(
`DELETE FROM sendbox WHERE address = ?`
).bind(address).run();
if (!success) {
return c.text("Failed to clear sent items", 500)
}
return c.json({
success: success
})
})
api.post('/api/address_change_password', address_auth.changePassword)
api.post('/api/address_login', address_auth.login)
+2 -2
View File
@@ -7,7 +7,7 @@ import { commonParseMail, sendWebhook } from "../common";
async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response> { async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
const { address } = c.get("jwtPayload") const { address } = c.get("jwtPayload")
const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json"); const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
if (adminSettings?.enableAllowList && !adminSettings?.allowList.includes(address)) { if (!adminSettings?.allowList.includes(address)) {
return c.text("Webhook settings is not allowed for this user", 403); return c.text("Webhook settings is not allowed for this user", 403);
} }
const settings = await c.env.KV.get<WebhookSettings>( const settings = await c.env.KV.get<WebhookSettings>(
@@ -20,7 +20,7 @@ async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response>
async function saveWebhookSettings(c: Context<HonoCustomType>): Promise<Response> { async function saveWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
const { address } = c.get("jwtPayload") const { address } = c.get("jwtPayload")
const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json"); const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
if (adminSettings?.enableAllowList && !adminSettings?.allowList.includes(address)) { if (!adminSettings?.allowList.includes(address)) {
return c.text("Webhook settings is not allowed for this user", 403); return c.text("Webhook settings is not allowed for this user", 403);
} }
const settings = await c.req.json<WebhookSettings>(); const settings = await c.req.json<WebhookSettings>();
+1 -12
View File
@@ -14,11 +14,9 @@ export type Passkey = {
}; };
export class AdminWebhookSettings { export class AdminWebhookSettings {
enableAllowList: boolean;
allowList: string[]; allowList: string[];
constructor(enableAllowList: boolean, allowList: string[]) { constructor(allowList: string[]) {
this.enableAllowList = enableAllowList;
this.allowList = allowList; this.allowList = allowList;
} }
} }
@@ -46,8 +44,6 @@ export type CleanupSettings = {
cleanAddressDays: number; cleanAddressDays: number;
enableInactiveAddressAutoCleanup: boolean | undefined; enableInactiveAddressAutoCleanup: boolean | undefined;
cleanInactiveAddressDays: number; cleanInactiveAddressDays: number;
enableUnboundAddressAutoCleanup: boolean | undefined;
cleanUnboundAddressDays: number;
} }
export class GeoData { export class GeoData {
@@ -154,10 +150,3 @@ export type EmailRuleSettings = {
blockReceiveUnknowAddressEmail: boolean; blockReceiveUnknowAddressEmail: boolean;
emailForwardingList: SubdomainForwardAddressList[] emailForwardingList: SubdomainForwardAddressList[]
} }
export type RoleConfig = {
maxAddressCount?: number;
// future configs can be added here
}
export type RoleAddressConfig = Record<string, RoleConfig>;
-7
View File
@@ -50,11 +50,4 @@ export async function scheduled(event: ScheduledEvent, env: Bindings, ctx: any)
autoCleanupSetting.cleanAddressDays autoCleanupSetting.cleanAddressDays
); );
} }
if (autoCleanupSetting.enableUnboundAddressAutoCleanup) {
await cleanup(
{ env: env, } as Context<HonoCustomType>,
"unboundAddress",
autoCleanupSetting.cleanUnboundAddressDays
);
}
} }
+1 -1
View File
@@ -6,7 +6,7 @@ import { deleteAddressWithData, newAddress, generateRandomName } from "../common
export const tgUserNewAddress = async ( export const tgUserNewAddress = async (
c: Context<HonoCustomType>, userId: string, address: string c: Context<HonoCustomType>, userId: string, address: string
): Promise<{ address: string, jwt: string, password?: string | null }> => { ): Promise<{ address: string, jwt: string }> => {
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(
{ key: `${CONSTANTS.TG_KV_PREFIX}:${userId}` } { key: `${CONSTANTS.TG_KV_PREFIX}:${userId}` }
-1
View File
@@ -101,7 +101,6 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
const res = await tgUserNewAddress(c, userId.toString(), address); const res = await tgUserNewAddress(c, userId.toString(), address);
return await ctx.reply(`创建地址成功:\n` return await ctx.reply(`创建地址成功:\n`
+ `地址: ${res.address}\n` + `地址: ${res.address}\n`
+ (res.password ? `密码: \`${res.password}\`\n` : '')
+ `凭证: \`${res.jwt}\`\n`, + `凭证: \`${res.jwt}\`\n`,
{ {
parse_mode: "Markdown" parse_mode: "Markdown"
-1
View File
@@ -40,7 +40,6 @@ type Bindings = {
ENABLE_USER_CREATE_EMAIL: string | boolean | undefined ENABLE_USER_CREATE_EMAIL: string | boolean | undefined
DISABLE_ANONYMOUS_USER_CREATE_EMAIL: string | boolean | undefined DISABLE_ANONYMOUS_USER_CREATE_EMAIL: string | boolean | undefined
ENABLE_USER_DELETE_EMAIL: string | boolean | undefined ENABLE_USER_DELETE_EMAIL: string | boolean | undefined
ENABLE_ADDRESS_PASSWORD: string | boolean | undefined
ENABLE_INDEX_ABOUT: string | boolean | undefined ENABLE_INDEX_ABOUT: string | boolean | undefined
DEFAULT_SEND_BALANCE: number | string | undefined DEFAULT_SEND_BALANCE: number | string | undefined
NO_LIMIT_SEND_ROLE: string | undefined | null NO_LIMIT_SEND_ROLE: string | undefined | null
+7 -29
View File
@@ -1,26 +1,12 @@
import { Context } from 'hono'; import { Context } from 'hono';
import { Jwt } from 'hono/utils/jwt' import { Jwt } from 'hono/utils/jwt'
import { UserSettings, RoleAddressConfig } from "../models"; import { UserSettings } from "../models";
import { getJsonSetting } from "../utils" import { getJsonSetting } from "../utils"
import { CONSTANTS } from "../constants"; import { CONSTANTS } from "../constants";
import { unbindTelegramByAddress } from '../telegram_api/common'; import { unbindTelegramByAddress } from '../telegram_api/common';
import i18n from '../i18n'; import i18n from '../i18n';
import { updateAddressUpdatedAt, commonGetUserRole } from '../common'; import { updateAddressUpdatedAt } from '../common';
const getMaxAddressCount = async (
c: Context<HonoCustomType>,
userRole: string | null | undefined,
settings: UserSettings
): Promise<number> => {
if (!userRole) return settings.maxAddressCount;
const roleConfigs = await getJsonSetting<RoleAddressConfig>(c, CONSTANTS.ROLE_ADDRESS_CONFIG_KEY);
if (!roleConfigs) return settings.maxAddressCount;
const roleMaxCount = roleConfigs[userRole]?.maxAddressCount;
if (typeof roleMaxCount !== 'number') return settings.maxAddressCount;
if (roleMaxCount <= 0) return settings.maxAddressCount;
return roleMaxCount;
};
const UserBindAddressModule = { const UserBindAddressModule = {
bind: async (c: Context<HonoCustomType>) => { bind: async (c: Context<HonoCustomType>) => {
@@ -57,15 +43,11 @@ const UserBindAddressModule = {
// check if binded address count // check if binded address count
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY); const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
const settings = new UserSettings(value); const settings = new UserSettings(value);
// get user role if (settings.maxAddressCount > 0) {
const userRole = c.get("userRolePayload");
// check role-based max address count first, fallback to global settings
const maxAddressCount = await getMaxAddressCount(c, userRole, settings);
if (maxAddressCount > 0) {
const { count } = await c.env.DB.prepare( const { count } = await c.env.DB.prepare(
`SELECT COUNT(*) as count FROM users_address where user_id = ?` `SELECT COUNT(*) as count FROM users_address where user_id = ?`
).bind(user_id).first<{ count: number }>() || { count: 0 }; ).bind(user_id).first<{ count: number }>() || { count: 0 };
if (count >= maxAddressCount) { if (count >= settings.maxAddressCount) {
return c.text("Max address count reached", 400) return c.text("Max address count reached", 400)
} }
} }
@@ -212,22 +194,18 @@ const UserBindAddressModule = {
// check if target user exists // check if target user exists
const target_user_id = await c.env.DB.prepare( const target_user_id = await c.env.DB.prepare(
`SELECT id FROM users where user_email = ?` `SELECT id FROM users where user_email = ?`
).bind(target_user_email).first<number>("id"); ).bind(target_user_email).first("id");
if (!target_user_id) { if (!target_user_id) {
return c.text("Target user not found", 400) return c.text("Target user not found", 400)
} }
// check target user binded address count // check target user binded address count
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY); const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
const settings = new UserSettings(value); const settings = new UserSettings(value);
// get target user role if (settings.maxAddressCount > 0) {
const userRoleObj = await commonGetUserRole(c, target_user_id);
// check role-based max address count first, fallback to global settings
const maxAddressCount = await getMaxAddressCount(c, userRoleObj?.role, settings);
if (maxAddressCount > 0) {
const { count } = await c.env.DB.prepare( const { count } = await c.env.DB.prepare(
`SELECT COUNT(*) as count FROM users_address where user_id = ?` `SELECT COUNT(*) as count FROM users_address where user_id = ?`
).bind(target_user_id).first<{ count: number }>() || { count: 0 }; ).bind(target_user_id).first<{ count: number }>() || { count: 0 };
if (count >= maxAddressCount) { if (count >= settings.maxAddressCount) {
return c.text("Target User Max address count reached", 400) return c.text("Target User Max address count reached", 400)
} }
} }
-7
View File
@@ -296,13 +296,6 @@ export const checkUserPassword = (password: string) => {
return true; return true;
} }
export const hashPassword = async (password: string): Promise<string> => {
// use crypto to hash password
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(password));
const hashArray = Array.from(new Uint8Array(digest));
return hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
}
export default { export default {
getJsonObjectValue, getJsonObjectValue,
getSetting, getSetting,
-7
View File
@@ -148,10 +148,6 @@ app.use('/api/*', async (c, next) => {
) { ) {
await checkoutUserRolePayload(c); await checkoutUserRolePayload(c);
} }
if (c.req.path.startsWith("/api/address_login")) {
await next();
return;
}
try { try {
return await jwt({ secret: c.env.JWT_SECRET, alg: "HS256" })(c, next); return await jwt({ secret: c.env.JWT_SECRET, alg: "HS256" })(c, next);
@@ -194,9 +190,6 @@ app.use('/user_api/*', async (c, next) => {
console.error(e); console.error(e);
return c.text(msgs.UserTokenExpiredMsg, 401) return c.text(msgs.UserTokenExpiredMsg, 401)
} }
if (c.req.path.startsWith("/user_api/bind_address")) {
await checkoutUserRolePayload(c);
}
if (c.req.path.startsWith('/user_api/bind_address') if (c.req.path.startsWith('/user_api/bind_address')
&& c.req.method === 'POST' && c.req.method === 'POST'
) { ) {
-2
View File
@@ -70,8 +70,6 @@ ENABLE_USER_DELETE_EMAIL = true
ENABLE_AUTO_REPLY = false ENABLE_AUTO_REPLY = false
# Allow webhook # Allow webhook
# ENABLE_WEBHOOK = true # ENABLE_WEBHOOK = true
# Enable address password feature, if set true, will generate password for new address and support password login and change
# ENABLE_ADDRESS_PASSWORD = false
# Footer text # Footer text
# COPYRIGHT = "Dream Hunter" # COPYRIGHT = "Dream Hunter"
# DISABLE_SHOW_GITHUB = true # DISABLE_SHOW_GITHUB = true