mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-05 07:27:27 +08:00
refactor: serve mail states from backend
This commit is contained in:
@@ -39,7 +39,7 @@ api.get('/open_api/settings', async (c) => {
|
||||
"disableAnonymousUserCreateEmail": utils.getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL),
|
||||
"disableCustomAddressName": utils.getBooleanValue(c.env.DISABLE_CUSTOM_ADDRESS_NAME),
|
||||
"enableUserDeleteEmail": utils.getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL),
|
||||
"enableReadStatus": utils.getBooleanValue(c.env.ENABLE_MAIL_FLAGS),
|
||||
"enableMailStates": utils.getBooleanValue(c.env.ENABLE_MAIL_FLAGS),
|
||||
"enableAutoReply": utils.getBooleanValue(c.env.ENABLE_AUTO_REPLY),
|
||||
"enableIndexAbout": utils.getBooleanValue(c.env.ENABLE_INDEX_ABOUT),
|
||||
"copyright": c.env.COPYRIGHT,
|
||||
|
||||
+4
-4
@@ -37,11 +37,11 @@ export async function resolveRawEmail(row: RawMailRow): Promise<string> {
|
||||
*/
|
||||
export async function resolveRawEmailRow(
|
||||
row: RawMailRow,
|
||||
enableReadStatus = false,
|
||||
enableMailStates = false,
|
||||
): Promise<RawMailRow> {
|
||||
const raw = await resolveRawEmail(row);
|
||||
const { raw_blob: _, ...rest } = row;
|
||||
return serializeMailState({ ...rest, raw }, enableReadStatus);
|
||||
return serializeMailState({ ...rest, raw }, enableMailStates);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +49,7 @@ export async function resolveRawEmailRow(
|
||||
*/
|
||||
export async function resolveRawEmailList(
|
||||
rows: RawMailRow[],
|
||||
enableReadStatus = false,
|
||||
enableMailStates = false,
|
||||
): Promise<RawMailRow[]> {
|
||||
return Promise.all(rows.map(row => resolveRawEmailRow(row, enableReadStatus)));
|
||||
return Promise.all(rows.map(row => resolveRawEmailRow(row, enableMailStates)));
|
||||
}
|
||||
|
||||
+111
-40
@@ -10,7 +10,59 @@ export const MAIL_FLAGS = {
|
||||
export const CUSTOM_MAIL_FLAG_OFFSET = 10;
|
||||
export const CUSTOM_MAIL_FLAG_COUNT = 10;
|
||||
|
||||
type MailReadStatusAction = 'read' | 'unread' | 'toggle';
|
||||
export enum MailState {
|
||||
ALL = 'all',
|
||||
UNREAD = 'unread',
|
||||
READ = 'read',
|
||||
}
|
||||
|
||||
export type MailStateOption = {
|
||||
value: string;
|
||||
label_key?: string;
|
||||
label?: string;
|
||||
unread?: boolean;
|
||||
default?: boolean;
|
||||
};
|
||||
|
||||
export type MailStateDefinition = MailStateOption & {
|
||||
filter?: { mask: number; set: boolean };
|
||||
mutation?: { add: number; remove: number };
|
||||
};
|
||||
|
||||
const SYSTEM_MAIL_STATES: MailStateDefinition[] = [
|
||||
{ value: MailState.ALL, label_key: 'allMail', default: true },
|
||||
{
|
||||
value: MailState.UNREAD,
|
||||
label_key: 'unread',
|
||||
unread: true,
|
||||
filter: { mask: MAIL_FLAGS.UNREAD, set: true },
|
||||
mutation: { add: MAIL_FLAGS.UNREAD, remove: 0 },
|
||||
},
|
||||
{
|
||||
value: MailState.READ,
|
||||
label_key: 'read',
|
||||
unread: false,
|
||||
filter: { mask: MAIL_FLAGS.UNREAD, set: false },
|
||||
mutation: { add: 0, remove: MAIL_FLAGS.UNREAD },
|
||||
},
|
||||
];
|
||||
|
||||
export const getMailStateOptions = (
|
||||
customStates: MailStateDefinition[] = [],
|
||||
): MailStateOption[] => {
|
||||
return [...SYSTEM_MAIL_STATES, ...customStates].map(state => {
|
||||
const { filter: _filter, mutation: _mutation, ...option } = state;
|
||||
return option;
|
||||
});
|
||||
};
|
||||
|
||||
const getMailStateDefinition = (
|
||||
value: unknown,
|
||||
customStates: MailStateDefinition[] = [],
|
||||
) => {
|
||||
if (typeof value !== 'string') return undefined;
|
||||
return [...SYSTEM_MAIL_STATES, ...customStates].find(state => state.value === value);
|
||||
};
|
||||
|
||||
export const getCustomMailFlag = (slot: number): number => {
|
||||
if (!Number.isInteger(slot) || slot < 0 || slot >= CUSTOM_MAIL_FLAG_COUNT) {
|
||||
@@ -19,6 +71,27 @@ export const getCustomMailFlag = (slot: number): number => {
|
||||
return 1 << (CUSTOM_MAIL_FLAG_OFFSET + slot);
|
||||
};
|
||||
|
||||
export type CustomMailStateConfig = {
|
||||
slot: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const CUSTOM_MAIL_FLAGS_MASK = ((1 << CUSTOM_MAIL_FLAG_COUNT) - 1) << CUSTOM_MAIL_FLAG_OFFSET;
|
||||
|
||||
export const createCustomMailStateDefinitions = (
|
||||
configs: CustomMailStateConfig[],
|
||||
): MailStateDefinition[] => {
|
||||
return configs.map(config => {
|
||||
const flag = getCustomMailFlag(config.slot);
|
||||
return {
|
||||
value: `custom:${config.slot}`,
|
||||
label: config.name,
|
||||
filter: { mask: flag, set: true },
|
||||
mutation: { add: flag, remove: CUSTOM_MAIL_FLAGS_MASK & ~flag },
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const serializeMailState = <T extends Record<string, unknown>>(
|
||||
row: T,
|
||||
enabled: boolean,
|
||||
@@ -54,10 +127,11 @@ export const updateInitialMailFlags = async (
|
||||
await db.prepare(`UPDATE raw_mails SET flags = ? WHERE id = ?`).bind(flags, mailId).run();
|
||||
};
|
||||
|
||||
export type MailReadStatusUpdate = {
|
||||
export type MailStateUpdate = {
|
||||
ids: number[];
|
||||
mask: number;
|
||||
action: MailReadStatusAction;
|
||||
state: string;
|
||||
add: number;
|
||||
remove: number;
|
||||
};
|
||||
|
||||
export type MailReadStatusQuery = {
|
||||
@@ -65,21 +139,26 @@ export type MailReadStatusQuery = {
|
||||
params: string[];
|
||||
};
|
||||
|
||||
export const getReadStatusQuery = (
|
||||
export const getMailStateQuery = (
|
||||
value: string | undefined,
|
||||
column: 'flags' | 'rm.flags',
|
||||
customStates: MailStateDefinition[] = [],
|
||||
): MailReadStatusQuery | undefined | null => {
|
||||
if (value === undefined || value === 'all') return undefined;
|
||||
if (value === 'unread') {
|
||||
return { clause: `(COALESCE(${column}, 0) & ?) != 0`, params: [String(MAIL_FLAGS.UNREAD)] };
|
||||
}
|
||||
if (value === 'read') {
|
||||
return { clause: `(COALESCE(${column}, 0) & ?) = 0`, params: [String(MAIL_FLAGS.UNREAD)] };
|
||||
}
|
||||
return null;
|
||||
if (value === undefined) return undefined;
|
||||
const definition = getMailStateDefinition(value, customStates);
|
||||
if (!definition) return null;
|
||||
if (!definition.filter) return undefined;
|
||||
const operator = definition.filter.set ? '!=' : '=';
|
||||
return {
|
||||
clause: `(COALESCE(${column}, 0) & ?) ${operator} 0`,
|
||||
params: [String(definition.filter.mask)],
|
||||
};
|
||||
};
|
||||
|
||||
const parseMailReadStatusUpdate = (value: unknown): MailReadStatusUpdate | null => {
|
||||
const parseMailStateUpdate = (
|
||||
value: unknown,
|
||||
customStates: MailStateDefinition[] = [],
|
||||
): MailStateUpdate | null => {
|
||||
if (!value || typeof value !== 'object') return null;
|
||||
const body = value as Record<string, unknown>;
|
||||
if (!Array.isArray(body.ids) || body.ids.length === 0 || body.ids.length > 100) return null;
|
||||
@@ -88,34 +167,25 @@ const parseMailReadStatusUpdate = (value: unknown): MailReadStatusUpdate | null
|
||||
const ids = [...new Set(body.ids.map(Number))];
|
||||
if (ids.some(id => !Number.isInteger(id) || id <= 0)) return null;
|
||||
|
||||
if (body.action !== 'read' && body.action !== 'unread' && body.action !== 'toggle') return null;
|
||||
|
||||
return { ids, mask: MAIL_FLAGS.UNREAD, action: body.action };
|
||||
const definition = getMailStateDefinition(body.state, customStates);
|
||||
if (!definition?.mutation) return null;
|
||||
return {
|
||||
ids,
|
||||
state: definition.value,
|
||||
add: definition.mutation.add,
|
||||
remove: definition.mutation.remove,
|
||||
};
|
||||
};
|
||||
|
||||
const getMailReadStatusUpdateExpression = (
|
||||
update: MailReadStatusUpdate,
|
||||
const getMailStateUpdateExpression = (
|
||||
update: MailStateUpdate,
|
||||
column = 'flags',
|
||||
): { expression: string; params: number[]; condition?: string; conditionParams?: number[] } => {
|
||||
if (update.action === 'unread') {
|
||||
return {
|
||||
expression: `(COALESCE(${column}, 0) | ?)`,
|
||||
params: [update.mask],
|
||||
condition: `(COALESCE(${column}, 0) & ?) = 0`,
|
||||
conditionParams: [update.mask],
|
||||
};
|
||||
}
|
||||
if (update.action === 'read') {
|
||||
return {
|
||||
expression: `(COALESCE(${column}, 0) & ~?)`,
|
||||
params: [update.mask],
|
||||
condition: `(COALESCE(${column}, 0) & ?) != 0`,
|
||||
conditionParams: [update.mask],
|
||||
};
|
||||
}
|
||||
return {
|
||||
expression: `((COALESCE(${column}, 0) | ?) - (COALESCE(${column}, 0) & ?))`,
|
||||
params: [update.mask, update.mask],
|
||||
expression: `((COALESCE(${column}, 0) | ?) & ~?)`,
|
||||
params: [update.add, update.remove],
|
||||
condition: `((COALESCE(${column}, 0) & ?) != ? OR (COALESCE(${column}, 0) & ?) != 0)`,
|
||||
conditionParams: [update.add, update.add, update.remove],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -124,16 +194,17 @@ type MailScope = {
|
||||
params: (string | number)[];
|
||||
};
|
||||
|
||||
export const applyMailReadStatusUpdate = async (
|
||||
export const applyMailStateUpdate = async (
|
||||
db: D1Database,
|
||||
scope: MailScope,
|
||||
value: unknown,
|
||||
customStates: MailStateDefinition[] = [],
|
||||
) => {
|
||||
const update = parseMailReadStatusUpdate(value);
|
||||
const update = parseMailStateUpdate(value, customStates);
|
||||
if (!update) return null;
|
||||
|
||||
const placeholders = update.ids.map(() => '?').join(',');
|
||||
const statusUpdate = getMailReadStatusUpdateExpression(update);
|
||||
const statusUpdate = getMailStateUpdateExpression(update);
|
||||
const condition = statusUpdate.condition ? ` AND ${statusUpdate.condition}` : '';
|
||||
const result = await db.prepare(
|
||||
`UPDATE raw_mails SET flags = ${statusUpdate.expression}`
|
||||
|
||||
@@ -27,8 +27,9 @@ api.post('/api/attachment/get_url', s3_attachment.getSignedGetUrl)
|
||||
|
||||
// mail crud
|
||||
api.get('/api/mails', mails_crud.listMails)
|
||||
api.get('/api/mail-states', mails_crud.getMailStates)
|
||||
api.get('/api/mail/:mail_id', mails_crud.getMail)
|
||||
api.patch('/api/mails/read-status', mails_crud.updateMailReadStatus)
|
||||
api.patch('/api/mails/state', mails_crud.updateMailState)
|
||||
api.delete('/api/mails/:id', mails_crud.deleteMail)
|
||||
|
||||
// parsed mail (server-side parsed subject/text/html/attachments)
|
||||
|
||||
@@ -6,8 +6,9 @@ import { handleMailListQuery, deleteAddressWithData, updateAddressUpdatedAt } fr
|
||||
import { resolveRawEmailRow } from '../gzip'
|
||||
import { getSendBalanceState } from './send_balance';
|
||||
import {
|
||||
getReadStatusQuery,
|
||||
applyMailReadStatusUpdate,
|
||||
getMailStateQuery,
|
||||
getMailStateOptions,
|
||||
applyMailStateUpdate,
|
||||
} from '../mail_flags';
|
||||
|
||||
const listMails = async (c: Context<HonoCustomType>) => {
|
||||
@@ -15,19 +16,19 @@ const listMails = async (c: Context<HonoCustomType>) => {
|
||||
if (!address) {
|
||||
return c.json({ "error": "No address" }, 400)
|
||||
}
|
||||
const { limit, offset, read_status } = c.req.query();
|
||||
const { limit, offset, mail_state } = c.req.query();
|
||||
if (Number.parseInt(offset) <= 0) updateAddressUpdatedAt(c, address);
|
||||
const readStatusQuery = getReadStatusQuery(read_status, 'flags');
|
||||
if (readStatusQuery === null) return c.json({ error: "Invalid mail read status filter" }, 400);
|
||||
if (readStatusQuery && !getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
||||
return c.json({ error: "Mail read status is disabled" }, 403);
|
||||
const stateQuery = getMailStateQuery(mail_state, 'flags');
|
||||
if (stateQuery === null) return c.json({ error: "Invalid mail state filter" }, 400);
|
||||
if (stateQuery && !getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
||||
return c.json({ error: "Mail states are disabled" }, 403);
|
||||
}
|
||||
|
||||
const filters = [`address = ?`];
|
||||
const params = [address];
|
||||
if (readStatusQuery) {
|
||||
filters.push(readStatusQuery.clause);
|
||||
params.push(...readStatusQuery.params);
|
||||
if (stateQuery) {
|
||||
filters.push(stateQuery.clause);
|
||||
params.push(...stateQuery.params);
|
||||
}
|
||||
const whereClause = filters.join(' AND ');
|
||||
return await handleMailListQuery(c,
|
||||
@@ -64,21 +65,28 @@ const deleteMail = async (c: Context<HonoCustomType>) => {
|
||||
return c.json({ success });
|
||||
};
|
||||
|
||||
const updateMailReadStatus = async (c: Context<HonoCustomType>) => {
|
||||
const updateMailState = async (c: Context<HonoCustomType>) => {
|
||||
if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
||||
return c.json({ error: "Mail read status is disabled" }, 403);
|
||||
return c.json({ error: "Mail states are disabled" }, 403);
|
||||
}
|
||||
const { address } = c.get("jwtPayload");
|
||||
const result = await applyMailReadStatusUpdate(
|
||||
const result = await applyMailStateUpdate(
|
||||
c.env.DB,
|
||||
{ clause: 'address = ?', params: [address] },
|
||||
await c.req.json().catch(() => null),
|
||||
);
|
||||
if (!result) return c.json({ error: "Invalid mail read status request" }, 400);
|
||||
if (!result) return c.json({ error: "Invalid mail state request" }, 400);
|
||||
if (!result.success) return c.json(result, 500);
|
||||
return c.json(result);
|
||||
};
|
||||
|
||||
const getMailStates = (c: Context<HonoCustomType>) => {
|
||||
if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
||||
return c.json({ error: "Mail states are disabled" }, 403);
|
||||
}
|
||||
return c.json({ results: getMailStateOptions() });
|
||||
};
|
||||
|
||||
const getSettings = async (c: Context<HonoCustomType>) => {
|
||||
const { address, address_id } = c.get("jwtPayload")
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
@@ -153,6 +161,6 @@ const clearSentItems = async (c: Context<HonoCustomType>) => {
|
||||
};
|
||||
|
||||
export default {
|
||||
listMails, getMail, deleteMail, updateMailReadStatus,
|
||||
listMails, getMail, deleteMail, updateMailState, getMailStates,
|
||||
getSettings, deleteAddress, clearInbox, clearSentItems
|
||||
};
|
||||
|
||||
@@ -16,7 +16,8 @@ api.get('/user_api/settings', settings.settings);
|
||||
|
||||
// mail api
|
||||
api.get('/user_api/mails', user_mail_api.getMails);
|
||||
api.patch('/user_api/mails/read-status', user_mail_api.updateMailReadStatus);
|
||||
api.get('/user_api/mail-states', user_mail_api.getMailStates);
|
||||
api.patch('/user_api/mails/state', user_mail_api.updateMailState);
|
||||
api.delete('/user_api/mails/:id', user_mail_api.deleteMail);
|
||||
|
||||
// send mail api
|
||||
|
||||
@@ -3,28 +3,35 @@ import i18n from "../i18n";
|
||||
import { handleMailListQuery } from "../common";
|
||||
import { getBooleanValue } from "../utils";
|
||||
import {
|
||||
getReadStatusQuery,
|
||||
applyMailReadStatusUpdate,
|
||||
getMailStateQuery,
|
||||
getMailStateOptions,
|
||||
applyMailStateUpdate,
|
||||
} from "../mail_flags";
|
||||
|
||||
export default {
|
||||
getMailStates: (c: Context<HonoCustomType>) => {
|
||||
if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
||||
return c.json({ error: "Mail states are disabled" }, 403);
|
||||
}
|
||||
return c.json({ results: getMailStateOptions() });
|
||||
},
|
||||
getMails: async (c: Context<HonoCustomType>) => {
|
||||
const { user_id } = c.get("userPayload");
|
||||
const { address, limit, offset, read_status } = c.req.query();
|
||||
const { address, limit, offset, mail_state } = c.req.query();
|
||||
const filterQuerys = [`ua.user_id = ?`];
|
||||
const filterParams = [String(user_id)];
|
||||
if (address) {
|
||||
filterQuerys.push(`rm.address = ?`);
|
||||
filterParams.push(address);
|
||||
}
|
||||
const readStatusQuery = getReadStatusQuery(read_status, 'rm.flags');
|
||||
if (readStatusQuery === null) return c.json({ error: "Invalid mail read status filter" }, 400);
|
||||
if (readStatusQuery && !getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
||||
return c.json({ error: "Mail read status is disabled" }, 403);
|
||||
const stateQuery = getMailStateQuery(mail_state, 'rm.flags');
|
||||
if (stateQuery === null) return c.json({ error: "Invalid mail state filter" }, 400);
|
||||
if (stateQuery && !getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
||||
return c.json({ error: "Mail states are disabled" }, 403);
|
||||
}
|
||||
if (readStatusQuery) {
|
||||
filterQuerys.push(readStatusQuery.clause);
|
||||
filterParams.push(...readStatusQuery.params);
|
||||
if (stateQuery) {
|
||||
filterQuerys.push(stateQuery.clause);
|
||||
filterParams.push(...stateQuery.params);
|
||||
}
|
||||
const fromQuery = ` FROM users_address ua`
|
||||
+ ` JOIN address a ON a.id = ua.address_id`
|
||||
@@ -55,12 +62,12 @@ export default {
|
||||
success: success
|
||||
})
|
||||
},
|
||||
updateMailReadStatus: async (c: Context<HonoCustomType>) => {
|
||||
updateMailState: async (c: Context<HonoCustomType>) => {
|
||||
if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
||||
return c.json({ error: "Mail read status is disabled" }, 403);
|
||||
return c.json({ error: "Mail states are disabled" }, 403);
|
||||
}
|
||||
const { user_id } = c.get("userPayload");
|
||||
const result = await applyMailReadStatusUpdate(
|
||||
const result = await applyMailStateUpdate(
|
||||
c.env.DB,
|
||||
{
|
||||
clause: `EXISTS (`
|
||||
@@ -72,7 +79,7 @@ export default {
|
||||
},
|
||||
await c.req.json().catch(() => null),
|
||||
);
|
||||
if (!result) return c.json({ error: "Invalid mail read status request" }, 400);
|
||||
if (!result) return c.json({ error: "Invalid mail state request" }, 400);
|
||||
if (!result.success) return c.json(result, 500);
|
||||
return c.json(result);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user