diff --git a/CHANGELOG.md b/CHANGELOG.md index ff5ef377..efd9f60b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ ## 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: |数据库| 为 `message_id` 字段添加索引,优化邮件更新操作性能,需执行 `db/2025-12-15-message-id-index.sql` 更新数据库 - feat: |Admin| 维护页面增加自定义 SQL 清理功能,支持定时任务执行自定义清理语句 diff --git a/CHANGELOG_EN.md b/CHANGELOG_EN.md index 20134bb7..c73b78ad 100644 --- a/CHANGELOG_EN.md +++ b/CHANGELOG_EN.md @@ -8,6 +8,13 @@ ## 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: |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 diff --git a/db/2025-12-27-source-meta.sql b/db/2025-12-27-source-meta.sql new file mode 100644 index 00000000..ab378914 --- /dev/null +++ b/db/2025-12-27-source-meta.sql @@ -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); diff --git a/db/schema.sql b/db/schema.sql index 7c2ccf4f..6160f757 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -18,6 +18,7 @@ CREATE TABLE IF NOT EXISTS address ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, password TEXT, + source_meta TEXT, created_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_source_meta ON address(source_meta); + CREATE TABLE IF NOT EXISTS auto_reply_mails ( id INTEGER PRIMARY KEY, source_prefix TEXT, diff --git a/frontend/src/views/admin/Account.vue b/frontend/src/views/admin/Account.vue index 980a5f91..d6aec778 100644 --- a/frontend/src/views/admin/Account.vue +++ b/frontend/src/views/admin/Account.vue @@ -22,6 +22,7 @@ const { t } = useI18n({ updated_at: 'Update At', mail_count: 'Mail Count', send_count: 'Send Count', + source_meta: 'Source', showCredential: 'Show 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.', @@ -59,6 +60,7 @@ const { t } = useI18n({ updated_at: '更新时间', mail_count: '邮件数量', send_count: '发送数量', + source_meta: '来源', showCredential: '查看邮箱地址凭证', addressCredential: '邮箱地址凭证', addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。', @@ -319,6 +321,10 @@ const columns = [ title: t('updated_at'), key: "updated_at" }, + { + title: t('source_meta'), + key: "source_meta" + }, { title: t('mail_count'), key: "mail_count", diff --git a/worker/src/admin_api/db_api.ts b/worker/src/admin_api/db_api.ts index 80edafc7..35ef332c 100644 --- a/worker/src/admin_api/db_api.ts +++ b/worker/src/admin_api/db_api.ts @@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS address ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, password TEXT, + source_meta TEXT, created_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_source_meta ON address(source_meta); + CREATE TABLE IF NOT EXISTS auto_reply_mails ( id INTEGER PRIMARY KEY, source_prefix TEXT, @@ -138,14 +141,42 @@ export default { }, migrate: async (c: Context) => { 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 && version <= "v0.0.2") { + // migration to v0.0.3: add password column + const tableInfo = await c.env.DB.prepare( + `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") { - // migration from v0.0.3 to v0.0.4 - await c.env.DB.exec(`ALTER TABLE raw_mails ADD COLUMN metadata TEXT;`); + if (version && version <= "v0.0.3") { + // migration to v0.0.4: add metadata column + 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) { // remove all \r and \n characters from the query string diff --git a/worker/src/admin_api/index.ts b/worker/src/admin_api/index.ts index 81fea094..b149d177 100644 --- a/worker/src/admin_api/index.ts +++ b/worker/src/admin_api/index.ts @@ -57,6 +57,7 @@ api.post('/admin/new_address', async (c) => { addressPrefix: null, checkAllowDomains: false, enableCheckNameRegex: false, + sourceMeta: 'admin' }); return c.json(res); diff --git a/worker/src/common.ts b/worker/src/common.ts index 14483e8f..db1f1b49 100644 --- a/worker/src/common.ts +++ b/worker/src/common.ts @@ -123,6 +123,7 @@ export const newAddress = async ( addressPrefix = null, checkAllowDomains = true, enableCheckNameRegex = true, + sourceMeta = null, }: { name: string, domain: string | undefined | null, enablePrefix: boolean, @@ -130,6 +131,7 @@ export const newAddress = async ( addressPrefix?: string | undefined | null, checkAllowDomains?: boolean, enableCheckNameRegex?: boolean, + sourceMeta?: string | undefined | null, } ): Promise<{ address: string, jwt: string, password?: string | null }> => { // trim whitespace and remove special characters @@ -180,19 +182,30 @@ export const newAddress = async ( // create address name = name + "@" + domain; try { - const { success } = await c.env.DB.prepare( - `INSERT INTO address(name) VALUES(?)` - ).bind(name).run(); - if (!success) { + // Try insert with source_meta field first + const result = await c.env.DB.prepare( + `INSERT INTO address(name, source_meta) VALUES(?, ?)` + ).bind(name, sourceMeta).run(); + if (!result.success) { throw new Error("Failed to create address") } await updateAddressUpdatedAt(c, name); } catch (e) { 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") + } else { + throw new Error("Failed to create address") } - throw new Error("Failed to create address") } const address_id = await c.env.DB.prepare( `SELECT id FROM address where name = ?` diff --git a/worker/src/constants.ts b/worker/src/constants.ts index cef1bbfd..753fbf52 100644 --- a/worker/src/constants.ts +++ b/worker/src/constants.ts @@ -3,7 +3,7 @@ export const CONSTANTS = { // DB Version DB_VERSION_KEY: 'db_version', - DB_VERSION: "v0.0.4", + DB_VERSION: "v0.0.5", // DB settings ADDRESS_BLOCK_LIST_KEY: 'address_block_list', diff --git a/worker/src/mails_api/index.ts b/worker/src/mails_api/index.ts index 3a6eb712..a4a8d5d8 100644 --- a/worker/src/mails_api/index.ts +++ b/worker/src/mails_api/index.ts @@ -144,11 +144,17 @@ api.post('/api/new_address', async (c) => { } try { 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, { name, domain, enablePrefix: true, checkLengthByConfig: true, - addressPrefix + addressPrefix, + sourceMeta }); return c.json(res); } catch (e) { diff --git a/worker/src/telegram_api/common.ts b/worker/src/telegram_api/common.ts index 9791a6a8..0985591d 100644 --- a/worker/src/telegram_api/common.ts +++ b/worker/src/telegram_api/common.ts @@ -38,7 +38,8 @@ export const tgUserNewAddress = async ( const res = await newAddress(c, { name: finalName, domain, - enablePrefix: true + enablePrefix: true, + sourceMeta: `tg:${userId}` }); // for mail push to telegram await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, res.jwt]));