diff --git a/frontend/src/components/MailBox.vue b/frontend/src/components/MailBox.vue index 9508109c..be7b0461 100644 --- a/frontend/src/components/MailBox.vue +++ b/frontend/src/components/MailBox.vue @@ -371,8 +371,15 @@ onBeforeUnmount(() => { {{ t('reply') }} + + {{ showTextMail ? t('showHtmlMail') : t('showTextMail') }} + -
+
{{ curMail.text }}
+ +
diff --git a/worker/src/admin_api/admin_user_api.js b/worker/src/admin_api/admin_user_api.js index 9412704b..8a8ba644 100644 --- a/worker/src/admin_api/admin_user_api.js +++ b/worker/src/admin_api/admin_user_api.js @@ -1,6 +1,7 @@ import { CONSTANTS } from '../constants'; import { getJsonSetting, saveSetting, checkUserPassword, getDomains } from '../utils'; import { UserSettings, GeoData, UserInfo } from "../models"; +import { handleListQuery } from '../common' export default { getSetting: async (c) => { @@ -29,49 +30,23 @@ export default { }, getUsers: async (c) => { const { limit, offset, query } = c.req.query(); - if (!limit || limit < 0 || limit > 100) { - return c.text("Invalid limit", 400) - } - if (!offset || offset < 0) { - return c.text("Invalid offset", 400) - } if (query) { - const { results } = await c.env.DB.prepare( + return await handleListQuery(c, `SELECT u.id, u.user_email, u.created_at, u.updated_at,` + ` (SELECT COUNT(*) FROM users_address WHERE user_id = u.id) AS address_count` + ` FROM users u` - + ` where u.user_email like ?` - + ` order by u.id desc limit ? offset ?` - ).bind(`%${query}%`, limit, offset).all(); - let count = 0; - if (offset == 0) { - const { count: userCount } = await c.env.DB.prepare( - `SELECT count(*) as count FROM users where user_email like ?` - ).bind(`%${query}%`).first(); - count = userCount; - } - return c.json({ - results: results, - count: count - }) + + ` where u.user_email like ?`, + `SELECT count(*) as count FROM users where user_email like ?`, + [`%${query}%`], limit, offset + ); } - const { results } = await c.env.DB.prepare( + return await handleListQuery(c, `SELECT u.id, u.user_email, u.created_at, u.updated_at,` + ` (SELECT COUNT(*) FROM users_address WHERE user_id = u.id) AS address_count` - + ` FROM users u` - + ` order by u.id desc limit ? offset ?` - ).bind(limit, offset).all(); - let count = 0; - if (offset == 0) { - const { count: userCount } = await c.env.DB.prepare( - `SELECT count(*) as count FROM users` - ).first(); - count = userCount; - } - return c.json({ - results: results, - count: count - }) + + ` FROM users u`, + `SELECT count(*) as count FROM users`, + [], limit, offset + ); }, createUser: async (c) => { const { email, password } = await c.req.json(); diff --git a/worker/src/admin_api/index.js b/worker/src/admin_api/index.js index 643dcb26..d1bb7401 100644 --- a/worker/src/admin_api/index.js +++ b/worker/src/admin_api/index.js @@ -10,51 +10,25 @@ const api = new Hono() api.get('/admin/address', async (c) => { const { limit, offset, query } = c.req.query(); - if (!limit || limit < 0 || limit > 100) { - return c.text("Invalid limit", 400) - } - if (!offset || offset < 0) { - return c.text("Invalid offset", 400) - } if (query) { - const { results } = await c.env.DB.prepare( + return await handleListQuery(c, `SELECT a.*,` + ` (SELECT COUNT(*) FROM raw_mails WHERE address = a.name) AS mail_count,` + ` (SELECT COUNT(*) FROM sendbox WHERE address = a.name) AS send_count` + ` FROM address a` - + ` where name like ?` - + ` order by id desc limit ? offset ?` - ).bind(`%${query}%`, limit, offset).all(); - let count = 0; - if (offset == 0) { - const { count: addressCount } = await c.env.DB.prepare( - `SELECT count(*) as count FROM address where name like ?` - ).bind(`%${query}%`).first(); - count = addressCount; - } - return c.json({ - results: results, - count: count - }) + + ` where name like ?`, + `SELECT count(*) as count FROM address where name like ?`, + [`%${query}%`], limit, offset + ); } - const { results } = await c.env.DB.prepare( + return await handleListQuery(c, `SELECT a.*,` + ` (SELECT COUNT(*) FROM raw_mails WHERE address = a.name) AS mail_count,` + ` (SELECT COUNT(*) FROM sendbox WHERE address = a.name) AS send_count` - + ` FROM address a` - + ` order by id desc limit ? offset ?` - ).bind(limit, offset).all(); - let count = 0; - if (offset == 0) { - const { count: addressCount } = await c.env.DB.prepare( - `SELECT count(*) as count FROM address` - ).first(); - count = addressCount; - } - return c.json({ - results: results, - count: count - }) + + ` FROM address a`, + `SELECT count(*) as count FROM address`, + [], limit, offset + ); }) api.post('/admin/new_address', async (c) => { @@ -100,7 +74,7 @@ api.get('/admin/show_password/:id', async (c) => { const jwt = await Jwt.sign({ address: name, address_id: id - }, c.env.JWT_SECRET) + }, c.env.JWT_SECRET, "HS256") return c.json({ jwt: jwt }) @@ -184,42 +158,18 @@ api.post('/admin/address_sender', async (c) => { api.get('/admin/sendbox', async (c) => { const { address, limit, offset } = c.req.query(); - if (!limit || limit < 0 || limit > 100) { - return c.text("Invalid limit", 400) + if (address) { + return await handleListQuery(c, + `SELECT * FROM sendbox where address = ? `, + `SELECT count(*) as count FROM sendbox where address = ? `, + [address], limit, offset + ); } - if (!offset || offset < 0) { - return c.text("Invalid offset", 400) - } - if (!address) { - const { results } = await c.env.DB.prepare( - `SELECT * FROM sendbox order by id desc limit ? offset ?` - ).bind(limit, offset).all(); - let count = 0; - if (offset == 0) { - const { count: mailCount } = await c.env.DB.prepare( - `SELECT count(*) as count FROM sendbox` - ).first(); - count = mailCount; - } - return c.json({ - results: results, - count: count - }) - } - const { results } = await c.env.DB.prepare( - `SELECT * FROM sendbox where address = ? order by id desc limit ? offset ?` - ).bind(address, limit, offset).all(); - let count = 0; - if (offset == 0) { - const { count: mailCount } = await c.env.DB.prepare( - `SELECT count(*) as count FROM sendbox where address = ?` - ).bind(address).first(); - count = mailCount; - } - return c.json({ - results: results, - count: count - }) + return await handleListQuery(c, + `SELECT * FROM sendbox `, + `SELECT count(*) as count FROM sendbox `, + [], limit, offset + ); }) api.get('/admin/statistics', async (c) => { diff --git a/worker/src/commom_api.js b/worker/src/commom_api.js new file mode 100644 index 00000000..99472b8d --- /dev/null +++ b/worker/src/commom_api.js @@ -0,0 +1,28 @@ +import { Hono } from 'hono' + +import { getDomains, getPasswords, getBooleanValue } from './utils'; + +const api = new Hono() + +api.get('/open_api/settings', async (c) => { + // check header x-custom-auth + let needAuth = false; + const passwords = getPasswords(c); + if (passwords && passwords.length > 0) { + const auth = c.req.raw.headers.get("x-custom-auth"); + needAuth = !passwords.includes(auth); + } + return c.json({ + "prefix": c.env.PREFIX, + "domains": getDomains(c), + "needAuth": needAuth, + "adminContact": c.env.ADMIN_CONTACT, + "enableUserCreateEmail": getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL), + "enableUserDeleteEmail": getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL), + "enableAutoReply": getBooleanValue(c.env.ENABLE_AUTO_REPLY), + "copyright": c.env.COPYRIGHT, + "cfTurnstileSiteKey": c.env.CF_TURNSTILE_SITE_KEY, + }); +}) + +export { api } diff --git a/worker/src/common.js b/worker/src/common.js index 7bdad7a3..2c785a90 100644 --- a/worker/src/common.js +++ b/worker/src/common.js @@ -48,7 +48,7 @@ export const newAddress = async (c, name, domain, enablePrefix) => { const jwt = await Jwt.sign({ address: name, address_id: address_id - }, c.env.JWT_SECRET) + }, c.env.JWT_SECRET, "HS256") return c.json({ jwt: jwt }) diff --git a/worker/src/mails_api/index.js b/worker/src/mails_api/index.js index b5ebac5a..f8a41c06 100644 --- a/worker/src/mails_api/index.js +++ b/worker/src/mails_api/index.js @@ -1,5 +1,8 @@ import { Hono } from 'hono' +import { getBooleanValue, getJsonSetting, checkCfTurnstile } from '../utils'; +import { newAddress, handleListQuery } from '../common' +import { CONSTANTS } from '../constants' import auto_reply from './auto_reply' const api = new Hono() @@ -7,4 +10,137 @@ const api = new Hono() api.get('/api/auto_reply', auto_reply.getAutoReply) api.post('/api/auto_reply', auto_reply.saveAutoReply) +api.get('/api/mails', async (c) => { + const { address } = c.get("jwtPayload") + if (!address) { + return c.json({ "error": "No address" }, 400) + } + const { limit, offset } = c.req.query(); + return await handleListQuery(c, + `SELECT * FROM raw_mails where address = ?`, + `SELECT count(*) as count FROM raw_mails where address = ?`, + [address], limit, offset + ); +}) + +api.delete('/api/mails/:id', async (c) => { + if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) { + return c.text("User delete email is disabled", 403) + } + const { address } = c.get("jwtPayload") + const { id } = c.req.param(); + const { success } = await c.env.DB.prepare( + `DELETE FROM raw_mails WHERE address = ? and id = ? ` + ).bind(address, id).run(); + return c.json({ + success: success + }) +}) + +api.get('/api/settings', async (c) => { + const { address, address_id } = c.get("jwtPayload") + if (address_id && address_id > 0) { + try { + const db_address_id = await c.env.DB.prepare( + `SELECT id FROM address where id = ? ` + ).bind(address_id).first("id"); + if (!db_address_id) { + return c.text("Invalid address", 400) + } + } catch (error) { + return c.text("Invalid address", 400) + } + } + // check address id + try { + if (!address_id) { + const db_address_id = await c.env.DB.prepare( + `SELECT id FROM address where name = ? ` + ).bind(address).first("id"); + if (!db_address_id) { + return c.text("Invalid address", 400) + } + } + } catch (error) { + return c.text("Invalid address", 400) + } + // update address updated_at + try { + c.env.DB.prepare( + `UPDATE address SET updated_at = datetime('now') where name = ?` + ).bind(address).run(); + } catch (e) { + console.warn("Failed to update address") + } + const { count: mailCountV1 } = await c.env.DB.prepare( + `SELECT count(*) as count FROM mails where address = ?` + ).bind(address).first(); + const balance = await c.env.DB.prepare( + `SELECT balance FROM address_sender + where address = ? and enabled = 1` + ).bind(address).first("balance"); + return c.json({ + address: address, + has_v1_mails: mailCountV1 && mailCountV1 > 0, + send_balance: balance || 0, + }); +}) + +api.post('/api/new_address', async (c) => { + if (!getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL)) { + return c.text("New address is disabled", 403) + } + let { name, domain, cf_token } = await c.req.json(); + // check cf turnstile + try { + await checkCfTurnstile(c, cf_token); + } catch (error) { + return c.text("Failed to check cf turnstile", 500) + } + // if no name, generate random name + if (!name) { + name = Math.random().toString(36).substring(2, 15); + } + // check name block list + try { + 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) + } + } catch (error) { + console.error(error); + } + return newAddress(c, name, domain, true); +}) + +api.delete('/api/delete_address', async (c) => { + if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) { + return c.text("User delete email is disabled", 403) + } + const { address, address_id } = c.get("jwtPayload") + let name = address; + const { success } = await c.env.DB.prepare( + `DELETE FROM address WHERE name = ? ` + ).bind(name).run(); + if (!success) { + return c.text("Failed to delete address", 500) + } + const { success: mailSuccess } = await c.env.DB.prepare( + `DELETE FROM raw_mails WHERE address = ? ` + ).bind(address).run(); + if (!mailSuccess) { + return c.text("Failed to delete mails", 500) + } + const { success: sendAccess } = await c.env.DB.prepare( + `DELETE FROM address_sender WHERE address = ? ` + ).bind(address).run(); + const { success: addressSuccess } = await c.env.DB.prepare( + `DELETE FROM users_address WHERE address_id = ? ` + ).bind(address_id).run(); + return c.json({ + success: success && mailSuccess && sendAccess && addressSuccess + }) +}) + export { api } diff --git a/worker/src/mails_api/send_mail_api.js b/worker/src/mails_api/send_mail_api.js index 9388996b..6ed3ea51 100644 --- a/worker/src/mails_api/send_mail_api.js +++ b/worker/src/mails_api/send_mail_api.js @@ -3,6 +3,8 @@ import { Jwt } from 'hono/utils/jwt' import { CONSTANTS } from '../constants' import { getJsonSetting, getDomains } from '../utils'; import { GeoData } from '../models' +import { handleListQuery } from '../common' + const api = new Hono() @@ -112,8 +114,7 @@ export const sendMail = async (c, address, reqJson) => { // update balance try { const { success } = await c.env.DB.prepare( - `UPDATE address_sender SET balance = balance - 1 - where address = ?` + `UPDATE address_sender SET balance = balance - 1 where address = ?` ).bind(address).run(); if (!success) { console.warn(`Failed to update balance for ${address}`); @@ -171,27 +172,11 @@ const getSendbox = async (c, address, limit, offset) => { if (!address) { return c.json({ "error": "No address" }, 400) } - if (!limit || limit < 0 || limit > 100) { - return c.text("Invalid limit", 400) - } - if (!offset || offset < 0) { - return c.text("Invalid offset", 400) - } - const { results } = await c.env.DB.prepare( - `SELECT * FROM sendbox where address = ? - order by id desc limit ? offset ?` - ).bind(address, limit, offset).all(); - let count = 0; - if (offset == 0) { - const { count: mailCount } = await c.env.DB.prepare( - `SELECT count(*) as count FROM sendbox where address = ?` - ).bind(address).first(); - count = mailCount; - } - return c.json({ - results: results, - count: count - }) + return await handleListQuery(c, + `SELECT * FROM sendbox where address = ? `, + `SELECT count(*) as count FROM sendbox where address = ? `, + [address], limit, offset + ); } api.get('/api/sendbox', async (c) => { diff --git a/worker/src/router.js b/worker/src/router.js deleted file mode 100644 index df07bf90..00000000 --- a/worker/src/router.js +++ /dev/null @@ -1,181 +0,0 @@ -import { Hono } from 'hono' - -import { - getDomains, getPasswords, getBooleanValue, getJsonSetting, - checkCfTurnstile -} from './utils'; -import { newAddress } from './common' -import { CONSTANTS } from './constants' - -const api = new Hono() - -api.get('/api/mails', async (c) => { - const { address } = c.get("jwtPayload") - if (!address) { - return c.json({ "error": "No address" }, 400) - } - const { limit, offset } = c.req.query(); - if (!limit || limit < 0 || limit > 100) { - return c.text("Invalid limit", 400) - } - if (!offset || offset < 0) { - return c.text("Invalid offset", 400) - } - const { results } = await c.env.DB.prepare( - `SELECT * FROM raw_mails where address = ? order by id desc limit ? offset ?` - ).bind(address, limit, offset).all(); - let count = 0; - if (offset == 0) { - const { count: mailCount } = await c.env.DB.prepare( - `SELECT count(*) as count FROM raw_mails where address = ?` - ).bind(address).first(); - count = mailCount; - } - return c.json({ - results: results, - count: count - }) -}) - -api.delete('/api/mails/:id', async (c) => { - if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) { - return c.text("User delete email is disabled", 403) - } - const { address } = c.get("jwtPayload") - const { id } = c.req.param(); - const { success } = await c.env.DB.prepare( - `DELETE FROM raw_mails WHERE address = ? and id = ?` - ).bind(address, id).run(); - return c.json({ - success: success - }) -}) - -api.get('/api/settings', async (c) => { - const { address, address_id } = c.get("jwtPayload") - if (address_id && address_id > 0) { - try { - const db_address_id = await c.env.DB.prepare( - `SELECT id FROM address where id = ?` - ).bind(address_id).first("id"); - if (!db_address_id) { - return c.text("Invalid address", 400) - } - } catch (error) { - return c.text("Invalid address", 400) - } - } - // check address id - try { - if (!address_id) { - const db_address_id = await c.env.DB.prepare( - `SELECT id FROM address where name = ?` - ).bind(address).first("id"); - if (!db_address_id) { - return c.text("Invalid address", 400) - } - } - } catch (error) { - return c.text("Invalid address", 400) - } - // update address updated_at - try { - c.env.DB.prepare( - `UPDATE address SET updated_at = datetime('now') where name = ?` - ).bind(address).run(); - } catch (e) { - console.warn("Failed to update address") - } - const { count: mailCountV1 } = await c.env.DB.prepare( - `SELECT count(*) as count FROM mails where address = ?` - ).bind(address).first(); - const balance = await c.env.DB.prepare( - `SELECT balance FROM address_sender - where address = ? and enabled = 1` - ).bind(address).first("balance"); - return c.json({ - address: address, - has_v1_mails: mailCountV1 && mailCountV1 > 0, - send_balance: balance || 0, - }); -}) - -api.get('/open_api/settings', async (c) => { - // check header x-custom-auth - let needAuth = false; - const passwords = getPasswords(c); - if (passwords && passwords.length > 0) { - const auth = c.req.raw.headers.get("x-custom-auth"); - needAuth = !passwords.includes(auth); - } - return c.json({ - "prefix": c.env.PREFIX, - "domains": getDomains(c), - "needAuth": needAuth, - "adminContact": c.env.ADMIN_CONTACT, - "enableUserCreateEmail": getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL), - "enableUserDeleteEmail": getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL), - "enableAutoReply": getBooleanValue(c.env.ENABLE_AUTO_REPLY), - "copyright": c.env.COPYRIGHT, - "cfTurnstileSiteKey": c.env.CF_TURNSTILE_SITE_KEY, - }); -}) - -api.post('/api/new_address', async (c) => { - if (!getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL)) { - return c.text("New address is disabled", 403) - } - let { name, domain, cf_token } = await c.req.json(); - // check cf turnstile - try { - await checkCfTurnstile(c, cf_token); - } catch (error) { - return c.text("Failed to check cf turnstile", 500) - } - // if no name, generate random name - if (!name) { - name = Math.random().toString(36).substring(2, 15); - } - // check name block list - try { - 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) - } - } catch (error) { - console.error(error); - } - return newAddress(c, name, domain, true); -}) - -api.delete('/api/delete_address', async (c) => { - if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) { - return c.text("User delete email is disabled", 403) - } - const { address, address_id } = c.get("jwtPayload") - let name = address; - const { success } = await c.env.DB.prepare( - `DELETE FROM address WHERE name = ? ` - ).bind(name).run(); - if (!success) { - return c.text("Failed to delete address", 500) - } - const { success: mailSuccess } = await c.env.DB.prepare( - `DELETE FROM raw_mails WHERE address = ? ` - ).bind(address).run(); - if (!mailSuccess) { - return c.text("Failed to delete mails", 500) - } - const { success: sendAccess } = await c.env.DB.prepare( - `DELETE FROM address_sender WHERE address = ? ` - ).bind(address).run(); - const { success: addressSuccess } = await c.env.DB.prepare( - `DELETE FROM users_address WHERE address_id = ?` - ).bind(address_id).run(); - return c.json({ - success: success && mailSuccess && sendAccess && addressSuccess - }) -}) - -export { api } diff --git a/worker/src/user_api/bind_address.js b/worker/src/user_api/bind_address.js index 985e4531..46585ccb 100644 --- a/worker/src/user_api/bind_address.js +++ b/worker/src/user_api/bind_address.js @@ -131,7 +131,7 @@ export default { const jwt = await Jwt.sign({ address: name, address_id: address_id - }, c.env.JWT_SECRET) + }, c.env.JWT_SECRET, "HS256") return c.json({ jwt: jwt }) diff --git a/worker/src/user_api/user.js b/worker/src/user_api/user.js index 066a9341..500d8f1c 100644 --- a/worker/src/user_api/user.js +++ b/worker/src/user_api/user.js @@ -136,7 +136,7 @@ export default { // 30 days expire in seconds exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, iat: Math.floor(Date.now() / 1000), - }, c.env.JWT_SECRET) + }, c.env.JWT_SECRET, "HS256") return c.json({ jwt: jwt }) diff --git a/worker/src/worker.js b/worker/src/worker.js index bad78dc9..c04d9c0b 100644 --- a/worker/src/worker.js +++ b/worker/src/worker.js @@ -3,8 +3,8 @@ import { cors } from 'hono/cors'; import { jwt } from 'hono/jwt' import { Jwt } from 'hono/utils/jwt' -import { api } from './router'; -import { api as MailsApi } from './mails_api' +import { api as commonApi } from './commom_api'; +import { api as mailsApi } from './mails_api' import { api as userApi } from './user_api'; import { api as adminApi } from './admin_api'; import { api as apiV1 } from './deprecated'; @@ -101,8 +101,8 @@ app.use('/admin/*', async (c, next) => { }); -app.route('/', api) -app.route('/', MailsApi) +app.route('/', commonApi) +app.route('/', mailsApi) app.route('/', userApi) app.route('/', adminApi) app.route('/', apiV1)