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
+25
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 })
}
}
+11 -47
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
})
+35 -2
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;
}
+1
View File
@@ -1,3 +1,4 @@
export const CONSTANTS = {
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
AUTO_CLEANUP_KEY: 'auto_cleanup',
}
+5 -49
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
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
);
}
}
+52
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
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 }
+40
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;
+4
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,
}
+4
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