mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-05 23:47:50 +08:00
feat: add address source tracking (source_meta field) (#794)
- Add source_meta field to address table for tracking creation source
- Web: records client IP address (with fallback to 'web:unknown')
- Telegram: records 'tg:{userId}'
- Admin: records 'admin'
- Add database migration with field existence check
- Add frontend display in admin Account page
- Backward compatible: fallback if field doesn't exist
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,13 @@
|
|||||||
|
|
||||||
## v1.2.0(main)
|
## v1.2.0(main)
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- |数据库| 新增 `source_meta` 字段,需执行 `db/2025-12-27-source-meta.sql` 更新数据库或到 admin 维护页面点击数据库更新按钮
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- feat: |地址来源| 新增地址来源追踪功能,记录地址创建来源(Web 记录 IP,Telegram 记录用户 ID,Admin 后台标记)
|
||||||
- feat: |邮件过滤| 移除后端 keyword 参数,改为前端过滤当前页邮件,优化查询性能
|
- feat: |邮件过滤| 移除后端 keyword 参数,改为前端过滤当前页邮件,优化查询性能
|
||||||
- feat: |数据库| 为 `message_id` 字段添加索引,优化邮件更新操作性能,需执行 `db/2025-12-15-message-id-index.sql` 更新数据库
|
- feat: |数据库| 为 `message_id` 字段添加索引,优化邮件更新操作性能,需执行 `db/2025-12-15-message-id-index.sql` 更新数据库
|
||||||
- feat: |Admin| 维护页面增加自定义 SQL 清理功能,支持定时任务执行自定义清理语句
|
- feat: |Admin| 维护页面增加自定义 SQL 清理功能,支持定时任务执行自定义清理语句
|
||||||
|
|||||||
@@ -8,6 +8,13 @@
|
|||||||
|
|
||||||
## v1.2.0(main)
|
## v1.2.0(main)
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- |Database| Add `source_meta` field, need to execute `db/2025-12-27-source-meta.sql` to update database or click database update button on admin maintenance page
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- feat: |Address Source| Add address source tracking feature, record address creation source (Web records IP, Telegram records user ID, Admin panel marked)
|
||||||
- feat: |Email Filtering| Remove backend keyword parameter, switch to frontend filtering of current page emails, optimize query performance
|
- feat: |Email Filtering| Remove backend keyword parameter, switch to frontend filtering of current page emails, optimize query performance
|
||||||
- feat: |Database| Add index for `message_id` field to optimize email update operations, need to execute `db/2025-12-15-message-id-index.sql` to update database
|
- feat: |Database| Add index for `message_id` field to optimize email update operations, need to execute `db/2025-12-15-message-id-index.sql` to update database
|
||||||
- feat: |Admin| Add custom SQL cleanup feature to maintenance page, support scheduled task execution of custom cleanup statements
|
- feat: |Admin| Add custom SQL cleanup feature to maintenance page, support scheduled task execution of custom cleanup statements
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- Add source_meta column to address table for tracking address creation source
|
||||||
|
-- For web: stores IP address (e.g., "192.168.1.1") or "web:unknown" as fallback
|
||||||
|
-- For telegram: stores "tg:{userId}" (e.g., "tg:123456789")
|
||||||
|
-- For admin: stores "admin"
|
||||||
|
|
||||||
|
ALTER TABLE address ADD COLUMN source_meta TEXT;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_address_source_meta ON address(source_meta);
|
||||||
@@ -18,6 +18,7 @@ CREATE TABLE IF NOT EXISTS address (
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT UNIQUE,
|
name TEXT UNIQUE,
|
||||||
password TEXT,
|
password TEXT,
|
||||||
|
source_meta 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
|
||||||
);
|
);
|
||||||
@@ -28,6 +29,8 @@ 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 INDEX IF NOT EXISTS idx_address_updated_at ON address(updated_at);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_address_source_meta ON address(source_meta);
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const { t } = useI18n({
|
|||||||
updated_at: 'Update At',
|
updated_at: 'Update At',
|
||||||
mail_count: 'Mail Count',
|
mail_count: 'Mail Count',
|
||||||
send_count: 'Send Count',
|
send_count: 'Send Count',
|
||||||
|
source_meta: 'Source',
|
||||||
showCredential: 'Show Mail Address Credential',
|
showCredential: 'Show Mail Address Credential',
|
||||||
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.',
|
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login to your email account.',
|
||||||
@@ -59,6 +60,7 @@ const { t } = useI18n({
|
|||||||
updated_at: '更新时间',
|
updated_at: '更新时间',
|
||||||
mail_count: '邮件数量',
|
mail_count: '邮件数量',
|
||||||
send_count: '发送数量',
|
send_count: '发送数量',
|
||||||
|
source_meta: '来源',
|
||||||
showCredential: '查看邮箱地址凭证',
|
showCredential: '查看邮箱地址凭证',
|
||||||
addressCredential: '邮箱地址凭证',
|
addressCredential: '邮箱地址凭证',
|
||||||
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
||||||
@@ -319,6 +321,10 @@ const columns = [
|
|||||||
title: t('updated_at'),
|
title: t('updated_at'),
|
||||||
key: "updated_at"
|
key: "updated_at"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t('source_meta'),
|
||||||
|
key: "source_meta"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t('mail_count'),
|
title: t('mail_count'),
|
||||||
key: "mail_count",
|
key: "mail_count",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS address (
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT UNIQUE,
|
name TEXT UNIQUE,
|
||||||
password TEXT,
|
password TEXT,
|
||||||
|
source_meta 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
|
||||||
);
|
);
|
||||||
@@ -31,6 +32,8 @@ 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 INDEX IF NOT EXISTS idx_address_updated_at ON address(updated_at);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_address_source_meta ON address(source_meta);
|
||||||
|
|
||||||
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,
|
||||||
@@ -138,14 +141,42 @@ 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") {
|
if (version && version <= "v0.0.2") {
|
||||||
// example migration from v0.0.2 to v0.0.3
|
// migration to v0.0.3: add password column
|
||||||
const query = `ALTER TABLE address ADD password TEXT;`
|
const tableInfo = await c.env.DB.prepare(
|
||||||
await c.env.DB.exec(query);
|
`PRAGMA table_info(address)`
|
||||||
|
).all();
|
||||||
|
const hasPassword = tableInfo.results?.some(
|
||||||
|
(col: any) => col.name === 'password'
|
||||||
|
);
|
||||||
|
if (!hasPassword) {
|
||||||
|
await c.env.DB.exec(`ALTER TABLE address ADD COLUMN password TEXT;`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (version == "v0.0.3") {
|
if (version && version <= "v0.0.3") {
|
||||||
// migration from v0.0.3 to v0.0.4
|
// migration to v0.0.4: add metadata column
|
||||||
await c.env.DB.exec(`ALTER TABLE raw_mails ADD COLUMN metadata TEXT;`);
|
const tableInfo = await c.env.DB.prepare(
|
||||||
|
`PRAGMA table_info(raw_mails)`
|
||||||
|
).all();
|
||||||
|
const hasMetadata = tableInfo.results?.some(
|
||||||
|
(col: any) => col.name === 'metadata'
|
||||||
|
);
|
||||||
|
if (!hasMetadata) {
|
||||||
|
await c.env.DB.exec(`ALTER TABLE raw_mails ADD COLUMN metadata TEXT;`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (version && version <= "v0.0.4") {
|
||||||
|
// migration to v0.0.5: add source_meta column
|
||||||
|
const tableInfo = await c.env.DB.prepare(
|
||||||
|
`PRAGMA table_info(address)`
|
||||||
|
).all();
|
||||||
|
const hasSourceMeta = tableInfo.results?.some(
|
||||||
|
(col: any) => col.name === 'source_meta'
|
||||||
|
);
|
||||||
|
if (!hasSourceMeta) {
|
||||||
|
await c.env.DB.exec(`ALTER TABLE address ADD COLUMN source_meta TEXT;`);
|
||||||
|
await c.env.DB.exec(`CREATE INDEX IF NOT EXISTS idx_address_source_meta ON address(source_meta);`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (version != CONSTANTS.DB_VERSION) {
|
if (version != CONSTANTS.DB_VERSION) {
|
||||||
// remove all \r and \n characters from the query string
|
// remove all \r and \n characters from the query string
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ api.post('/admin/new_address', async (c) => {
|
|||||||
addressPrefix: null,
|
addressPrefix: null,
|
||||||
checkAllowDomains: false,
|
checkAllowDomains: false,
|
||||||
enableCheckNameRegex: false,
|
enableCheckNameRegex: false,
|
||||||
|
sourceMeta: 'admin'
|
||||||
});
|
});
|
||||||
|
|
||||||
return c.json(res);
|
return c.json(res);
|
||||||
|
|||||||
+19
-6
@@ -123,6 +123,7 @@ export const newAddress = async (
|
|||||||
addressPrefix = null,
|
addressPrefix = null,
|
||||||
checkAllowDomains = true,
|
checkAllowDomains = true,
|
||||||
enableCheckNameRegex = true,
|
enableCheckNameRegex = true,
|
||||||
|
sourceMeta = null,
|
||||||
}: {
|
}: {
|
||||||
name: string, domain: string | undefined | null,
|
name: string, domain: string | undefined | null,
|
||||||
enablePrefix: boolean,
|
enablePrefix: boolean,
|
||||||
@@ -130,6 +131,7 @@ export const newAddress = async (
|
|||||||
addressPrefix?: string | undefined | null,
|
addressPrefix?: string | undefined | null,
|
||||||
checkAllowDomains?: boolean,
|
checkAllowDomains?: boolean,
|
||||||
enableCheckNameRegex?: boolean,
|
enableCheckNameRegex?: boolean,
|
||||||
|
sourceMeta?: string | undefined | null,
|
||||||
}
|
}
|
||||||
): Promise<{ address: string, jwt: string, password?: string | null }> => {
|
): Promise<{ address: string, jwt: string, password?: string | null }> => {
|
||||||
// trim whitespace and remove special characters
|
// trim whitespace and remove special characters
|
||||||
@@ -180,19 +182,30 @@ export const newAddress = async (
|
|||||||
// create address
|
// create address
|
||||||
name = name + "@" + domain;
|
name = name + "@" + domain;
|
||||||
try {
|
try {
|
||||||
const { success } = await c.env.DB.prepare(
|
// Try insert with source_meta field first
|
||||||
`INSERT INTO address(name) VALUES(?)`
|
const result = await c.env.DB.prepare(
|
||||||
).bind(name).run();
|
`INSERT INTO address(name, source_meta) VALUES(?, ?)`
|
||||||
if (!success) {
|
).bind(name, sourceMeta).run();
|
||||||
|
if (!result.success) {
|
||||||
throw new Error("Failed to create address")
|
throw new Error("Failed to create address")
|
||||||
}
|
}
|
||||||
await updateAddressUpdatedAt(c, name);
|
await updateAddressUpdatedAt(c, name);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const message = (e as Error).message;
|
const message = (e as Error).message;
|
||||||
if (message && message.includes("UNIQUE")) {
|
// Fallback: source_meta field may not exist, try without it
|
||||||
|
if (message && message.includes("source_meta")) {
|
||||||
|
const result = await c.env.DB.prepare(
|
||||||
|
`INSERT INTO address(name) VALUES(?)`
|
||||||
|
).bind(name).run();
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error("Failed to create address")
|
||||||
|
}
|
||||||
|
await updateAddressUpdatedAt(c, name);
|
||||||
|
} else if (message && message.includes("UNIQUE")) {
|
||||||
throw new Error("Address already exists")
|
throw new Error("Address already exists")
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to create address")
|
||||||
}
|
}
|
||||||
throw new Error("Failed to create address")
|
|
||||||
}
|
}
|
||||||
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 = ?`
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ export const CONSTANTS = {
|
|||||||
|
|
||||||
// DB Version
|
// DB Version
|
||||||
DB_VERSION_KEY: 'db_version',
|
DB_VERSION_KEY: 'db_version',
|
||||||
DB_VERSION: "v0.0.4",
|
DB_VERSION: "v0.0.5",
|
||||||
|
|
||||||
// DB settings
|
// DB settings
|
||||||
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
||||||
|
|||||||
@@ -144,11 +144,17 @@ api.post('/api/new_address', async (c) => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const addressPrefix = await getAddressPrefix(c);
|
const addressPrefix = await getAddressPrefix(c);
|
||||||
|
// Get client IP for source tracking
|
||||||
|
const sourceMeta = c.req.header('CF-Connecting-IP')
|
||||||
|
|| c.req.header('X-Forwarded-For')?.split(',')[0]?.trim()
|
||||||
|
|| c.req.header('X-Real-IP')
|
||||||
|
|| 'web:unknown';
|
||||||
const res = await newAddress(c, {
|
const res = await newAddress(c, {
|
||||||
name, domain,
|
name, domain,
|
||||||
enablePrefix: true,
|
enablePrefix: true,
|
||||||
checkLengthByConfig: true,
|
checkLengthByConfig: true,
|
||||||
addressPrefix
|
addressPrefix,
|
||||||
|
sourceMeta
|
||||||
});
|
});
|
||||||
return c.json(res);
|
return c.json(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ export const tgUserNewAddress = async (
|
|||||||
const res = await newAddress(c, {
|
const res = await newAddress(c, {
|
||||||
name: finalName,
|
name: finalName,
|
||||||
domain,
|
domain,
|
||||||
enablePrefix: true
|
enablePrefix: true,
|
||||||
|
sourceMeta: `tg:${userId}`
|
||||||
});
|
});
|
||||||
// for mail push to telegram
|
// for mail push to telegram
|
||||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, res.jwt]));
|
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, res.jwt]));
|
||||||
|
|||||||
Reference in New Issue
Block a user