diff --git a/frontend/src/views/admin/AccountSettings.vue b/frontend/src/views/admin/AccountSettings.vue index df85c27b..6df8f6af 100644 --- a/frontend/src/views/admin/AccountSettings.vue +++ b/frontend/src/views/admin/AccountSettings.vue @@ -5,9 +5,7 @@ import { useI18n } from 'vue-i18n' import { useGlobalState } from '../../store' import { api } from '../../api' -const { - localeCache, loading, openSettings, -} = useGlobalState() +const { localeCache, loading } = useGlobalState() const message = useMessage() const { t } = useI18n({ diff --git a/frontend/src/views/admin/Maintenance.vue b/frontend/src/views/admin/Maintenance.vue index c738abe0..c97127fe 100644 --- a/frontend/src/views/admin/Maintenance.vue +++ b/frontend/src/views/admin/Maintenance.vue @@ -8,29 +8,41 @@ import { api } from '../../api' const { localeCache, adminAuth, showAdminAuth } = useGlobalState() const message = useMessage() -const cleanMailsDays = ref(30) -const cleanUnknowMailsDays = ref(30) -const cleanAddressDays = ref(30) -const cleanSendBoxDays = ref(30) +const cleanupModel = ref({ + enableMailsAutoCleanup: false, + cleanMailsDays: 30, + enableUnknowMailsAutoCleanup: false, + cleanUnknowMailsDays: 30, + enableAddressAutoCleanup: false, + cleanAddressDays: 30, + enableSendBoxAutoCleanup: false, + cleanSendBoxDays: 30, +}) const { t } = useI18n({ locale: localeCache.value || 'zh', messages: { en: { tip: 'Please input the cleanup days', - mailBoxTip: "Clean up {day} days ago mailbox", - mailUnknowTip: "Clean up {day} days ago mails with unknow receiver", - addressUnActiveTip: "Clean up {day} days ago unactive address", - sendBoxTip: "Clean up {day} days ago sendbox", + mailBoxLabel: 'Clean up days for mailbox', + mailUnknowLabel: "Clean up days for unknow receiver", + addressUnActiveLabel: "Clean up days for unactive address", + sendBoxLabel: "Clean up days for sendbox", + cleanupNow: "Cleanup now", + autoCleanup: "Auto cleanup", cleanupSuccess: "Cleanup success", + save: "Save", }, zh: { tip: '请输入清理天数', - mailBoxTip: "清理{day}天前的收件箱", - mailUnknowTip: "清理{day}天前的无收件人邮件", - addressUnActiveTip: "清理{day}天前的未活动地址", - sendBoxTip: "清理{day}天前的发件箱", + mailBoxLabel: '收件箱清理天数', + mailUnknowLabel: "无收件人邮件清理天数", + addressUnActiveLabel: "未激活地址清理天数", + sendBoxLabel: "发件箱清理天数", + autoCleanup: "自动清理", cleanupSuccess: "清理成功", + cleanupNow: "立即清理", + save: "保存", } } }); @@ -47,11 +59,33 @@ const cleanup = async (cleanType, cleanDays) => { } } +const fetchData = async () => { + try { + const res = await api.fetch('/admin/auto_cleanup'); + if (res) Object.assign(cleanupModel.value, res); + } catch (error) { + message.error(error.message || "error"); + } +} + +const save = async () => { + try { + await api.fetch('/admin/auto_cleanup', { + method: 'POST', + body: JSON.stringify(cleanupModel.value) + }); + message.success(t('cleanupSuccess')); + } catch (error) { + message.error(error.message || "error"); + } +} + onMounted(async () => { if (!adminAuth.value) { showAdminAuth.value = true; return; } + await fetchData(); }) @@ -59,42 +93,59 @@ onMounted(async () => { diff --git a/frontend/src/views/user/AutoReply.vue b/frontend/src/views/user/AutoReply.vue index 4cea1ba1..8ddbc678 100644 --- a/frontend/src/views/user/AutoReply.vue +++ b/frontend/src/views/user/AutoReply.vue @@ -41,17 +41,22 @@ const { t } = useI18n({ } }); -const getSettings = async () => { - sourcePrefix.value = settings.value.auto_reply.source_prefix || "" - enableAutoReply.value = settings.value.auto_reply.enabled || false - name.value = settings.value.auto_reply.name || "" - autoReplyMessage.value = settings.value.auto_reply.message || "" - subject.value = settings.value.auto_reply.subject || "" +const fetchData = async () => { + try { + const res = await api.fetch("/api/auto_reply") + sourcePrefix.value = res.source_prefix || "" + enableAutoReply.value = res.enabled || false + name.value = res.name || "" + autoReplyMessage.value = res.message || "" + subject.value = res.subject || "" + } catch (error) { + message.error(error.message || "error"); + } } -const saveSettings = async () => { +const saveData = async () => { try { - await api.fetch("/api/settings", { + await api.fetch("/api/auto_reply", { method: "POST", body: JSON.stringify({ auto_reply: { @@ -70,7 +75,7 @@ const saveSettings = async () => { } onMounted(async () => { - await getSettings() + await fetchData() }) @@ -78,7 +83,7 @@ onMounted(async () => {
- {{ t('save') }} + {{ t('save') }}
diff --git a/vitepress-docs/docs/en/cli.md b/vitepress-docs/docs/en/cli.md index 04a2e143..9661c3e6 100644 --- a/vitepress-docs/docs/en/cli.md +++ b/vitepress-docs/docs/en/cli.md @@ -56,12 +56,16 @@ pnpm run deploy `wrangler.toml` -```bash +```toml name = "cloudflare_temp_email" main = "src/worker.js" compatibility_date = "2023-08-14" node_compat = true +# enable cron if you want set auto clean up +# [triggers] +# crons = [ "0 0 * * *" ] + [vars] PREFIX = "tmp" # The mailbox name prefix to be processed # If you want your site to be private, uncomment below and change your password diff --git a/vitepress-docs/docs/zh/guide/cli/worker.md b/vitepress-docs/docs/zh/guide/cli/worker.md index ec2426aa..8f927bb2 100644 --- a/vitepress-docs/docs/zh/guide/cli/worker.md +++ b/vitepress-docs/docs/zh/guide/cli/worker.md @@ -20,6 +20,10 @@ compatibility_date = "2023-12-01" # ] node_compat = true +# 如果你想要使用定时任务清理邮件,取消下面的注释,并修改 cron 表达式 +# [triggers] +# crons = [ "0 0 * * *" ] + [vars] PREFIX = "tmp" # 要处理的邮箱名称前缀,不需要后缀可配置为空字符串 # 如果你想要你的网站私有,取消下面的注释,并修改密码 diff --git a/worker/src/admin/cleanup_api.js b/worker/src/admin/cleanup_api.js new file mode 100644 index 00000000..c0d02ba8 --- /dev/null +++ b/worker/src/admin/cleanup_api.js @@ -0,0 +1,25 @@ +import { cleanup } from '../common'; +import { CONSTANTS } from '../constants'; +import { getJsonSetting, saveSetting } from '../utils'; + +export default { + cleanup: async (c) => { + const { cleanType, cleanDays } = await c.req.json(); + try { + await cleanup(c, cleanType, cleanDays); + } catch (error) { + console.error(error); + return c.text(`Failed to cleanup ${error.message}`, 500) + } + return c.json({ success: true }) + }, + getCleanup: async (c) => { + const value = await getJsonSetting(c, CONSTANTS.AUTO_CLEANUP_KEY); + return c.json(value || {}) + }, + saveCleanup: async (c) => { + const value = await c.req.json(); + await saveSetting(c, CONSTANTS.AUTO_CLEANUP_KEY, JSON.stringify(value)); + return c.json({ success: true }) + } +} diff --git a/worker/src/admin_api.js b/worker/src/admin_api.js index f2208da5..7d1b8c29 100644 --- a/worker/src/admin_api.js +++ b/worker/src/admin_api.js @@ -1,8 +1,9 @@ import { Hono } from 'hono' import { Jwt } from 'hono/utils/jwt' -import { sendAdminInternalMail } from './utils' +import { sendAdminInternalMail, getJsonSetting, saveSetting } from './utils' import { newAddress } from './common' import { CONSTANTS } from './constants' +import cleanup_api from './admin/cleanup_api' const api = new Hono() @@ -294,49 +295,16 @@ api.get('/admin/statistics', async (c) => { }) }); -api.post('/admin/cleanup', async (c) => { - const { cleanType, cleanDays } = await c.req.json(); - if (!cleanType || !cleanDays || cleanDays < 0 || cleanDays > 30) { - return c.text("Invalid cleanType or cleanDays", 400) - } - console.log(`Cleanup ${cleanType} before ${cleanDays} days`); - switch (cleanType) { - case "mails": - await c.env.DB.prepare(` - DELETE FROM raw_mails WHERE created_at < datetime('now', '-${cleanDays} day')` - ).run(); - break; - case "mails_unknow": - await c.env.DB.prepare(` - DELETE FROM raw_mails WHERE address NOT IN - (select name from address) AND created_at < datetime('now', '-${cleanDays} day')` - ).run(); - break; - case "address": - await c.env.DB.prepare(` - DELETE FROM address WHERE updated_at < datetime('now', '-${cleanDays} day')` - ).run(); - break; - case "sendbox": - await c.env.DB.prepare(` - DELETE FROM sendbox WHERE created_at < datetime('now', '-${cleanDays} day')` - ).run(); - break; - default: - return c.text("Invalid cleanType", 400) - } - return c.json({ - success: true - }) -}) +api.post('/admin/cleanup', cleanup_api.cleanup) +api.get('/admin/auto_cleanup', cleanup_api.getCleanup) +api.post('/admin/auto_cleanup', cleanup_api.saveCleanup) + api.get('/admin/account_settings', async (c) => { try { - const value = await c.env.DB.prepare( - `SELECT value FROM settings where key = ?` - ).bind(CONSTANTS.ADDRESS_BLOCK_LIST_KEY).first("value"); + const value = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY); return c.json({ - blockList: value ? JSON.parse(value) : [] + blockList: value || [] }) } catch (error) { console.error(error); @@ -349,14 +317,10 @@ api.post('/admin/account_settings', async (c) => { if (!blockList) { return c.text("Invalid blockList", 400) } - await c.env.DB.prepare( - `INSERT or REPLACE INTO settings (key, value) VALUES (?, ?)` - + ` ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = datetime('now')` - ).bind( - CONSTANTS.ADDRESS_BLOCK_LIST_KEY, - JSON.stringify(blockList), + await saveSetting( + c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY, JSON.stringify(blockList) - ).run(); + ); return c.json({ success: true }) diff --git a/worker/src/common.js b/worker/src/common.js index d5977b96..946d2ddb 100644 --- a/worker/src/common.js +++ b/worker/src/common.js @@ -1,6 +1,6 @@ import { Jwt } from 'hono/utils/jwt' -import { getDomains } from './utils'; +import { getDomains, getStringValue } from './utils'; export const newAddress = async (c, name, domain, enablePrefix) => { // remove special characters @@ -19,7 +19,7 @@ export const newAddress = async (c, name, domain, enablePrefix) => { } // create address if (enablePrefix) { - name = c.env.PREFIX + name + "@" + domain; + name = getStringValue(c.env.PREFIX) + name + "@" + domain; } else { name = name + "@" + domain; } @@ -53,3 +53,36 @@ export const newAddress = async (c, name, domain, enablePrefix) => { jwt: jwt }) } + +export const cleanup = async (c, cleanType, cleanDays) => { + if (!cleanType || !cleanDays || cleanDays < 0 || cleanDays > 30) { + throw new Error("Invalid cleanType or cleanDays") + } + console.log(`Cleanup ${cleanType} before ${cleanDays} days`); + switch (cleanType) { + case "mails": + await c.env.DB.prepare(` + DELETE FROM raw_mails WHERE created_at < datetime('now', '-${cleanDays} day')` + ).run(); + break; + case "mails_unknow": + await c.env.DB.prepare(` + DELETE FROM raw_mails WHERE address NOT IN + (select name from address) AND created_at < datetime('now', '-${cleanDays} day')` + ).run(); + break; + case "address": + await c.env.DB.prepare(` + DELETE FROM address WHERE updated_at < datetime('now', '-${cleanDays} day')` + ).run(); + break; + case "sendbox": + await c.env.DB.prepare(` + DELETE FROM sendbox WHERE created_at < datetime('now', '-${cleanDays} day')` + ).run(); + break; + default: + throw new Error("Invalid cleanType") + } + return true; +} diff --git a/worker/src/constants.js b/worker/src/constants.js index 6f1630d6..2768998d 100644 --- a/worker/src/constants.js +++ b/worker/src/constants.js @@ -1,3 +1,4 @@ export const CONSTANTS = { ADDRESS_BLOCK_LIST_KEY: 'address_block_list', + AUTO_CLEANUP_KEY: 'auto_cleanup', } diff --git a/worker/src/router.js b/worker/src/router.js index b77eb756..b1fe679f 100644 --- a/worker/src/router.js +++ b/worker/src/router.js @@ -1,6 +1,8 @@ import { Hono } from 'hono' -import { getDomains, getPasswords, getBooleanValue } from './utils'; +import { + getDomains, getPasswords, getBooleanValue, getJsonSetting +} from './utils'; import { newAddress } from './common' import { CONSTANTS } from './constants' @@ -83,21 +85,6 @@ api.get('/api/settings', async (c) => { } catch (e) { console.warn("Failed to update address") } - let auto_reply = {}; - if (getBooleanValue(c.env.ENABLE_AUTO_REPLY)) { - const results = await c.env.DB.prepare( - `SELECT * FROM auto_reply_mails where address = ? ` - ).bind(address).first(); - if (results) { - auto_reply = { - subject: results.subject, - message: results.message, - enabled: results.enabled == 1, - source_prefix: results.source_prefix, - name: results.name, - } - } - } const { count: mailCountV1 } = await c.env.DB.prepare( `SELECT count(*) as count FROM mails where address = ?` ).bind(address).first(); @@ -106,41 +93,12 @@ api.get('/api/settings', async (c) => { where address = ? and enabled = 1` ).bind(address).first("balance"); return c.json({ - auto_reply: auto_reply, address: address, has_v1_mails: mailCountV1 && mailCountV1 > 0, send_balance: balance || 0, }); }) -api.post('/api/settings', async (c) => { - const { address } = c.get("jwtPayload") - if (!getBooleanValue(c.env.ENABLE_AUTO_REPLY)) { - return c.text("Auto reply is disabled", 403) - } - const { auto_reply } = await c.req.json(); - const { name, subject, source_prefix, message, enabled } = auto_reply; - if ((!subject || !message) && enabled) { - return c.text("Invalid subject or message", 400) - } - else if (subject.length > 255 || message.length > 255) { - return c.text("Subject or message too long", 400) - } - const { success } = await c.env.DB.prepare( - `INSERT OR REPLACE INTO - auto_reply_mails - (name, address, source_prefix, subject, message, enabled) - VALUES - (?, ?, ?, ?, ?, ?)` - ).bind(name || '', address, source_prefix || '', subject || '', message || '', enabled ? 1 : 0).run(); - if (!success) { - return c.text("Failed to save settings", 500) - } - return c.json({ - success: success - }) -}) - api.get('/open_api/settings', async (c) => { // check header x-custom-auth let needAuth = false; @@ -172,10 +130,8 @@ api.get('/api/new_address', async (c) => { } // check name block list try { - const value = await c.env.DB.prepare( - `SELECT value FROM settings where key = ?` - ).bind(CONSTANTS.ADDRESS_BLOCK_LIST_KEY).first("value"); - const blockList = value ? JSON.parse(value) : []; + const value = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY); + const blockList = value || []; if (blockList.some((item) => name.includes(item))) { return c.text(`Name [${name}] is blocked`, 400) } diff --git a/worker/src/scheduled.js b/worker/src/scheduled.js new file mode 100644 index 00000000..139f5a94 --- /dev/null +++ b/worker/src/scheduled.js @@ -0,0 +1,41 @@ +import { cleanup } from './common' +import { CONSTANTS } from './constants' +import { getJsonSetting } from './utils'; + +export async function scheduled(event, env, ctx) { + console.log("Scheduled event: ", event); + let autoCleanupSetting = await getJsonSetting( + { env: env, }, + CONSTANTS.AUTO_CLEANUP_KEY + ); + console.log("autoCleanupSetting:", JSON.stringify(autoCleanupSetting)); + autoCleanupSetting = autoCleanupSetting || {}; + if (autoCleanupSetting.enableMailsAutoCleanup && autoCleanupSetting.cleanMailsDays > 0) { + await cleanup( + { env: env, }, + "mails", + autoCleanupSetting.cleanMailsDays + ); + } + if (autoCleanupSetting.enableUnknowMailsAutoCleanup && autoCleanupSetting.cleanUnknowMailsDays > 0) { + await cleanup( + { env: env, }, + "mails_unknow", + autoCleanupSetting.cleanUnknowMailsDays + ); + } + if (autoCleanupSetting.enableAddressAutoCleanup && autoCleanupSetting.cleanAddressDays > 0) { + await cleanup( + { env: env, }, + "address", + autoCleanupSetting.cleanAddressDays + ); + } + if (autoCleanupSetting.enableSendBoxAutoCleanup && autoCleanupSetting.cleanSendBoxDays > 0) { + await cleanup( + { env: env, }, + "sendbox", + autoCleanupSetting.cleanSendBoxDays + ); + } +} diff --git a/worker/src/user/auto_reply.js b/worker/src/user/auto_reply.js new file mode 100644 index 00000000..bd671564 --- /dev/null +++ b/worker/src/user/auto_reply.js @@ -0,0 +1,52 @@ +import { getBooleanValue } from "../utils"; + + +export default { + getAutoReply: async (c) => { + if (!getBooleanValue(c.env.ENABLE_AUTO_REPLY)) { + return c.text("Auto reply is disabled", 403) + } + const { address } = c.get("jwtPayload") + const results = await c.env.DB.prepare( + `SELECT * FROM auto_reply_mails where address = ? ` + ).bind(address).first(); + if (!results) { + return c.json({}); + } + return c.json({ + subject: results.subject, + message: results.message, + enabled: results.enabled == 1, + source_prefix: results.source_prefix, + name: results.name, + }) + }, + saveAutoReply: async (c) => { + if (!getBooleanValue(c.env.ENABLE_AUTO_REPLY)) { + return c.text("Auto reply is disabled", 403) + } + const { address } = c.get("jwtPayload") + const { auto_reply } = await c.req.json(); + const { name, subject, source_prefix, message, enabled } = auto_reply; + if ((!subject || !message) && enabled) { + return c.text("Invalid subject or message", 400) + } + else if (subject.length > 255 || message.length > 255) { + return c.text("Subject or message too long", 400) + } + const { success } = await c.env.DB.prepare( + `INSERT OR REPLACE INTO auto_reply_mails` + + ` (name, address, source_prefix, subject, message, enabled)` + + ` VALUES (?, ?, ?, ?, ?, ?)` + ).bind( + name || '', address, source_prefix || '', + subject || '', message || '', enabled ? 1 : 0 + ).run(); + if (!success) { + return c.text("Failed to auto_reply settings", 500) + } + return c.json({ + success: success + }) + } +} diff --git a/worker/src/user_api.js b/worker/src/user_api.js new file mode 100644 index 00000000..6a2b881e --- /dev/null +++ b/worker/src/user_api.js @@ -0,0 +1,10 @@ +import { Hono } from 'hono' + +import auto_reply from './user/auto_reply' + +const api = new Hono() + +api.get('/api/auto_reply', auto_reply.getAutoReply) +api.post('/api/auto_reply', auto_reply.saveAutoReply) + +export { api } diff --git a/worker/src/utils.js b/worker/src/utils.js index a33612a4..72c646c0 100644 --- a/worker/src/utils.js +++ b/worker/src/utils.js @@ -1,5 +1,45 @@ import { createMimeMessage } from "mimetext"; +export const getJsonSetting = async (c, key) => { + const value = await getSetting(c, key); + if (!value) { + return null; + } + try { + return JSON.parse(value); + } catch (e) { + console.error(`GetJsonSetting: Failed to parse ${key}`, e); + } + return null; +} + +export const getSetting = async (c, key) => { + try { + const value = await c.env.DB.prepare( + `SELECT value FROM settings where key = ?` + ).bind(key).first("value"); + return value; + } catch (error) { + console.error(`GetSetting: Failed to get ${key}`, error); + } + return null; +} + +export const saveSetting = async (c, key, value) => { + await c.env.DB.prepare( + `INSERT or REPLACE INTO settings (key, value) VALUES (?, ?)` + + ` ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = datetime('now')` + ).bind(key, value, value).run(); + return true; +} + +export const getStringValue = (value) => { + if (typeof value === "string") { + return value; + } + return ""; +} + export const getBooleanValue = (value) => { if (typeof value === "boolean") { return value; diff --git a/worker/src/worker.js b/worker/src/worker.js index 69c225b5..b8a420fc 100644 --- a/worker/src/worker.js +++ b/worker/src/worker.js @@ -3,10 +3,12 @@ import { cors } from 'hono/cors'; import { jwt } from 'hono/jwt' import { api } from './router'; +import { api as userApi } from './user_api'; import { api as adminApi } from './admin_api'; import { api as apiV1 } from './api_v1'; import { api as apiSendMail } from './send_mail_api' import { email } from './email'; +import { scheduled } from './scheduled'; import { getAdminPasswords, getPasswords } from './utils'; const app = new Hono() @@ -53,6 +55,7 @@ app.use('/admin/*', async (c, next) => { app.route('/', api) +app.route('/', userApi) app.route('/', adminApi) app.route('/', apiV1) app.route('/', apiSendMail) @@ -65,4 +68,5 @@ app.all('/*', async c => c.text("Not Found", 404)) export default { fetch: app.fetch, email: email, + scheduled: scheduled, } diff --git a/worker/wrangler.toml.template b/worker/wrangler.toml.template index 3b3a3799..6e2be371 100644 --- a/worker/wrangler.toml.template +++ b/worker/wrangler.toml.template @@ -7,6 +7,10 @@ node_compat = true # { pattern = "temp-email-api.xxxxx.xyz", custom_domain = true }, # ] +# enable cron if you want set auto clean up +# [triggers] +# crons = [ "0 0 * * *" ] + [vars] PREFIX = "tmp" # IF YOU WANT TO MAKE YOUR SITE PRIVATE, UNCOMMENT THE FOLLOWING LINES