refactor: serve mail states from backend

This commit is contained in:
dreamhunter2333
2026-08-26 00:05:02 +08:00
parent b5d8f64320
commit 04138ebcdb
18 changed files with 291 additions and 155 deletions
+111 -40
View File
@@ -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}`