feat: add cron auto clean up (#189)

This commit is contained in:
Dream Hunter
2024-05-03 22:05:44 +08:00
committed by GitHub
parent 6e02e9b20b
commit 51ad37e951
16 changed files with 351 additions and 159 deletions

View File

@@ -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({

View File

@@ -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();
})
</script>
@@ -59,42 +93,59 @@ onMounted(async () => {
<template>
<div class="center">
<n-card>
<div class="item">
<n-input-number v-model:value="cleanMailsDays" :placeholder="t('tip')" />
<n-button @click="cleanup('mails', cleanMailsDays)">
<template #icon>
<n-icon :component="CleaningServicesFilled" />
</template>
{{ t('mailBoxTip', { day: cleanMailsDays }) }}
<n-form :model="cleanupModel">
<n-form-item-row :label="t('mailBoxLabel')">
<n-checkbox v-model:checked="cleanupModel.enableMailsAutoCleanup">
{{ t('autoCleanup') }}
</n-checkbox>
<n-input-number v-model:value="cleanupModel.cleanMailsDays" :placeholder="t('tip')" />
<n-button @click="cleanup('mails', cleanupModel.cleanMailsDays)">
<template #icon>
<n-icon :component="CleaningServicesFilled" />
</template>
{{ t('cleanupNow') }}
</n-button>
</n-form-item-row>
<n-form-item-row :label="t('mailUnknowLabel')">
<n-checkbox v-model:checked="cleanupModel.enableUnknowMailsAutoCleanup">
{{ t('autoCleanup') }}
</n-checkbox>
<n-input-number v-model:value="cleanupModel.cleanUnknowMailsDays" :placeholder="t('tip')" />
<n-button @click="cleanup('mails_unknow', cleanupModel.cleanUnknowMailsDays)">
<template #icon>
<n-icon :component="CleaningServicesFilled" />
</template>
{{ t('cleanupNow') }}
</n-button>
</n-form-item-row>
<n-form-item-row :label="t('addressUnActiveLabel')">
<n-checkbox v-model:checked="cleanupModel.enableAddressAutoCleanup">
{{ t('autoCleanup') }}
</n-checkbox>
<n-input-number v-model:value="cleanupModel.cleanAddressDays" :placeholder="t('tip')" />
<n-button @click="cleanup('address', cleanupModel.cleanAddressDays)">
<template #icon>
<n-icon :component="CleaningServicesFilled" />
</template>
{{ t('cleanupNow') }}
</n-button>
</n-form-item-row>
<n-form-item-row :label="t('mailBoxLabel')">
<n-checkbox v-model:checked="cleanupModel.enableSendBoxAutoCleanup">
{{ t('autoCleanup') }}
</n-checkbox>
<n-input-number v-model:value="cleanupModel.cleanSendBoxDays" :placeholder="t('tip')" />
<n-button @click="cleanup('sendbox', cleanupModel.cleanSendBoxDays)">
<template #icon>
<n-icon :component="CleaningServicesFilled" />
</template>
{{ t('cleanupNow') }}
</n-button>
</n-form-item-row>
<n-button @click="save" type="primary" block :loading="loading">
{{ t('save') }}
</n-button>
</div>
<div class="item">
<n-input-number v-model:value="cleanUnknowMailsDays" :placeholder="t('tip')" />
<n-button @click="cleanup('mails_unknow', cleanUnknowMailsDays)">
<template #icon>
<n-icon :component="CleaningServicesFilled" />
</template>
{{ t('mailUnknowTip', { day: cleanUnknowMailsDays }) }}
</n-button>
</div>
<div class="item">
<n-input-number v-model:value="cleanAddressDays" :placeholder="t('tip')" />
<n-button @click="cleanup('address', cleanAddressDays)">
<template #icon>
<n-icon :component="CleaningServicesFilled" />
</template>
{{ t('addressUnActiveTip', { day: cleanAddressDays }) }}
</n-button>
</div>
<div class="item">
<n-input-number v-model:value="cleanSendBoxDays" :placeholder="t('tip')" />
<n-button @click="cleanup('sendbox', cleanSendBoxDays)">
<template #icon>
<n-icon :component="CleaningServicesFilled" />
</template>
{{ t('sendBoxTip', { day: cleanSendBoxDays }) }}
</n-button>
</div>
</n-form>
</n-card>
</div>
</template>

View File

@@ -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()
})
</script>
@@ -78,7 +83,7 @@ onMounted(async () => {
<div class="center">
<n-card v-if="settings.address" :title='t("settings")'>
<div class="right">
<n-button type="primary" @click="saveSettings">{{ t('save') }}</n-button>
<n-button type="primary" @click="saveData">{{ t('save') }}</n-button>
</div>
<div class="left">
<n-form-item :label="t('enableAutoReply')" label-placement="left">

View File

@@ -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

View File

@@ -20,6 +20,10 @@ compatibility_date = "2023-12-01"
# ]
node_compat = true
# 如果你想要使用定时任务清理邮件,取消下面的注释,并修改 cron 表达式
# [triggers]
# crons = [ "0 0 * * *" ]
[vars]
PREFIX = "tmp" # 要处理的邮箱名称前缀,不需要后缀可配置为空字符串
# 如果你想要你的网站私有,取消下面的注释,并修改密码

View File

@@ -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 })
}
}

View File

@@ -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
})

View File

@@ -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;
}

View File

@@ -1,3 +1,4 @@
export const CONSTANTS = {
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
AUTO_CLEANUP_KEY: 'auto_cleanup',
}

View File

@@ -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)
}

41
worker/src/scheduled.js Normal file
View File

@@ -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
);
}
}

View File

@@ -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
})
}
}

10
worker/src/user_api.js Normal file
View File

@@ -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 }

View File

@@ -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;

View File

@@ -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,
}

View File

@@ -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