mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-08-29 03:57:05 +08:00
refactor: move mail flag logic to backend
This commit is contained in:
+63
-20
@@ -9,7 +9,18 @@ export const MAIL_FLAGS = {
|
||||
|
||||
export const CUSTOM_MAIL_FLAG_OFFSET = 10;
|
||||
export const CUSTOM_MAIL_FLAG_COUNT = 10;
|
||||
export const MUTABLE_MAIL_FLAGS = MAIL_FLAGS.UNREAD;
|
||||
|
||||
const MAIL_FLAG_MASKS = {
|
||||
unread: MAIL_FLAGS.UNREAD,
|
||||
} as const;
|
||||
|
||||
type MailFlagName = keyof typeof MAIL_FLAG_MASKS;
|
||||
type MailFlagAction = 'set' | 'clear' | 'toggle';
|
||||
|
||||
const isMailFlagName = (value: unknown): value is MailFlagName => {
|
||||
return typeof value === 'string'
|
||||
&& Object.prototype.hasOwnProperty.call(MAIL_FLAG_MASKS, value);
|
||||
};
|
||||
|
||||
export const getCustomMailFlag = (slot: number): number => {
|
||||
if (!Number.isInteger(slot) || slot < 0 || slot >= CUSTOM_MAIL_FLAG_COUNT) {
|
||||
@@ -23,11 +34,14 @@ export const serializeMailFlags = <T extends Record<string, unknown>>(
|
||||
enabled: boolean,
|
||||
): T => {
|
||||
const result = { ...row };
|
||||
const flags = Number(result.flags ?? 0);
|
||||
delete result.flags;
|
||||
if (!enabled) {
|
||||
delete result.flags;
|
||||
return result;
|
||||
}
|
||||
result.flags = Number(result.flags ?? 0);
|
||||
result.mail_flags = {
|
||||
unread: (flags & MAIL_FLAGS.UNREAD) !== 0,
|
||||
};
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -67,8 +81,9 @@ export const insertRawMail = async (
|
||||
|
||||
export type MailFlagUpdate = {
|
||||
ids: number[];
|
||||
add: number;
|
||||
remove: number;
|
||||
flag: MailFlagName;
|
||||
mask: number;
|
||||
action: MailFlagAction;
|
||||
};
|
||||
|
||||
export type MailFlagFilter = {
|
||||
@@ -77,15 +92,22 @@ export type MailFlagFilter = {
|
||||
};
|
||||
|
||||
export const parseMailFlagFilter = (
|
||||
bitValue: string | undefined,
|
||||
flagValue: string | undefined,
|
||||
stateValue: string | undefined,
|
||||
): MailFlagFilter | undefined | null => {
|
||||
if (bitValue === undefined && stateValue === undefined) return undefined;
|
||||
if (!bitValue || !/^\d+$/.test(bitValue)) return null;
|
||||
const bit = Number(bitValue);
|
||||
if (!Number.isInteger(bit) || bit < 0 || bit > 30) return null;
|
||||
if (flagValue === undefined && stateValue === undefined) return undefined;
|
||||
if (!isMailFlagName(flagValue)) return null;
|
||||
if (stateValue !== 'set' && stateValue !== 'unset') return null;
|
||||
return { mask: 1 << bit, state: stateValue };
|
||||
return { mask: MAIL_FLAG_MASKS[flagValue], state: stateValue };
|
||||
};
|
||||
|
||||
export const parseReadStatusFilter = (
|
||||
value: string | undefined,
|
||||
): MailFlagFilter | undefined | null => {
|
||||
if (value === undefined || value === 'all') return undefined;
|
||||
if (value === 'unread') return { mask: MAIL_FLAGS.UNREAD, state: 'set' };
|
||||
if (value === 'read') return { mask: MAIL_FLAGS.UNREAD, state: 'unset' };
|
||||
return null;
|
||||
};
|
||||
|
||||
export const parseMailFlagUpdate = (value: unknown): MailFlagUpdate | null => {
|
||||
@@ -97,14 +119,35 @@ export const parseMailFlagUpdate = (value: unknown): MailFlagUpdate | null => {
|
||||
const ids = [...new Set(body.ids.map(Number))];
|
||||
if (ids.some(id => !Number.isInteger(id) || id <= 0)) return null;
|
||||
|
||||
if (body.add !== undefined && typeof body.add !== 'number') return null;
|
||||
if (body.remove !== undefined && typeof body.remove !== 'number') return null;
|
||||
const add = Number(body.add ?? 0);
|
||||
const remove = Number(body.remove ?? 0);
|
||||
if (!Number.isInteger(add) || !Number.isInteger(remove) || add < 0 || remove < 0) return null;
|
||||
if (add > MUTABLE_MAIL_FLAGS || remove > MUTABLE_MAIL_FLAGS) return null;
|
||||
if (((add | remove) & ~MUTABLE_MAIL_FLAGS) !== 0 || (add & remove) !== 0) return null;
|
||||
if (add === 0 && remove === 0) return null;
|
||||
if (!isMailFlagName(body.flag)) return null;
|
||||
if (body.action !== 'set' && body.action !== 'clear' && body.action !== 'toggle') return null;
|
||||
|
||||
return { ids, add, remove };
|
||||
const flag = body.flag;
|
||||
return { ids, flag, mask: MAIL_FLAG_MASKS[flag], action: body.action };
|
||||
};
|
||||
|
||||
export const getMailFlagUpdateExpression = (
|
||||
update: MailFlagUpdate,
|
||||
column = 'flags',
|
||||
): { expression: string; params: number[]; condition?: string; conditionParams?: number[] } => {
|
||||
if (update.action === 'set') {
|
||||
return {
|
||||
expression: `(COALESCE(${column}, 0) | ?)`,
|
||||
params: [update.mask],
|
||||
condition: `(COALESCE(${column}, 0) & ?) = 0`,
|
||||
conditionParams: [update.mask],
|
||||
};
|
||||
}
|
||||
if (update.action === 'clear') {
|
||||
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],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,16 +5,27 @@ import { getBooleanValue } from '../utils';
|
||||
import { handleMailListQuery, deleteAddressWithData, updateAddressUpdatedAt } from '../common'
|
||||
import { resolveRawEmailRow } from '../gzip'
|
||||
import { getSendBalanceState } from './send_balance';
|
||||
import { parseMailFlagFilter, parseMailFlagUpdate, serializeMailFlags } from '../mail_flags';
|
||||
import {
|
||||
getMailFlagUpdateExpression,
|
||||
parseMailFlagFilter,
|
||||
parseMailFlagUpdate,
|
||||
parseReadStatusFilter,
|
||||
serializeMailFlags,
|
||||
} from '../mail_flags';
|
||||
|
||||
const listMails = async (c: Context<HonoCustomType>) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
if (!address) {
|
||||
return c.json({ "error": "No address" }, 400)
|
||||
}
|
||||
const { limit, offset, flag, flag_state } = c.req.query();
|
||||
const { limit, offset, flag, flag_state, read_status } = c.req.query();
|
||||
if (Number.parseInt(offset) <= 0) updateAddressUpdatedAt(c, address);
|
||||
const flagFilter = parseMailFlagFilter(flag, flag_state);
|
||||
if (read_status !== undefined && (flag !== undefined || flag_state !== undefined)) {
|
||||
return c.json({ error: "Conflicting mail flag filters" }, 400);
|
||||
}
|
||||
const flagFilter = read_status === undefined
|
||||
? parseMailFlagFilter(flag, flag_state)
|
||||
: parseReadStatusFilter(read_status);
|
||||
if (flagFilter === null) return c.json({ error: "Invalid mail flag filter" }, 400);
|
||||
if (flagFilter && !getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
||||
return c.json({ error: "Mail flags are disabled" }, 403);
|
||||
@@ -68,12 +79,28 @@ const updateMailFlags = async (c: Context<HonoCustomType>) => {
|
||||
|
||||
const { address } = c.get("jwtPayload");
|
||||
const placeholders = update.ids.map(() => '?').join(',');
|
||||
const flagUpdate = getMailFlagUpdateExpression(update);
|
||||
const condition = flagUpdate.condition ? ` AND ${flagUpdate.condition}` : '';
|
||||
const result = await c.env.DB.prepare(
|
||||
`UPDATE raw_mails`
|
||||
+ ` SET flags = (COALESCE(flags, 0) | ?) & ~?`
|
||||
+ ` WHERE address = ? AND id IN (${placeholders})`
|
||||
).bind(update.add, update.remove, address, ...update.ids).run();
|
||||
return c.json({ success: result.success, changes: result.meta.changes ?? 0 });
|
||||
+ ` SET flags = ${flagUpdate.expression}`
|
||||
+ ` WHERE address = ? AND id IN (${placeholders})${condition}`
|
||||
).bind(
|
||||
...flagUpdate.params,
|
||||
address,
|
||||
...update.ids,
|
||||
...(flagUpdate.conditionParams ?? []),
|
||||
).run();
|
||||
if (!result.success) return c.json({ success: false, changes: 0, results: [] }, 500);
|
||||
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT id, flags FROM raw_mails WHERE address = ? AND id IN (${placeholders})`
|
||||
).bind(address, ...update.ids).all();
|
||||
return c.json({
|
||||
success: true,
|
||||
changes: result.meta.changes ?? 0,
|
||||
results: results.map(row => serializeMailFlags(row, true)),
|
||||
});
|
||||
};
|
||||
|
||||
const getSettings = async (c: Context<HonoCustomType>) => {
|
||||
|
||||
@@ -2,19 +2,30 @@ import { Context } from "hono";
|
||||
import i18n from "../i18n";
|
||||
import { handleMailListQuery } from "../common";
|
||||
import { getBooleanValue } from "../utils";
|
||||
import { parseMailFlagFilter, parseMailFlagUpdate } from "../mail_flags";
|
||||
import {
|
||||
getMailFlagUpdateExpression,
|
||||
parseMailFlagFilter,
|
||||
parseMailFlagUpdate,
|
||||
parseReadStatusFilter,
|
||||
serializeMailFlags,
|
||||
} from "../mail_flags";
|
||||
|
||||
export default {
|
||||
getMails: async (c: Context<HonoCustomType>) => {
|
||||
const { user_id } = c.get("userPayload");
|
||||
const { address, limit, offset, flag, flag_state } = c.req.query();
|
||||
const { address, limit, offset, flag, flag_state, read_status } = c.req.query();
|
||||
const filterQuerys = [`ua.user_id = ?`];
|
||||
const filterParams = [String(user_id)];
|
||||
if (address) {
|
||||
filterQuerys.push(`rm.address = ?`);
|
||||
filterParams.push(address);
|
||||
}
|
||||
const flagFilter = parseMailFlagFilter(flag, flag_state);
|
||||
if (read_status !== undefined && (flag !== undefined || flag_state !== undefined)) {
|
||||
return c.json({ error: "Conflicting mail flag filters" }, 400);
|
||||
}
|
||||
const flagFilter = read_status === undefined
|
||||
? parseMailFlagFilter(flag, flag_state)
|
||||
: parseReadStatusFilter(read_status);
|
||||
if (flagFilter === null) return c.json({ error: "Invalid mail flag filter" }, 400);
|
||||
if (flagFilter && !getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
||||
return c.json({ error: "Mail flags are disabled" }, 403);
|
||||
@@ -61,16 +72,38 @@ export default {
|
||||
|
||||
const { user_id } = c.get("userPayload");
|
||||
const placeholders = update.ids.map(() => '?').join(',');
|
||||
const flagUpdate = getMailFlagUpdateExpression(update);
|
||||
const condition = flagUpdate.condition ? ` AND ${flagUpdate.condition}` : '';
|
||||
const result = await c.env.DB.prepare(
|
||||
`UPDATE raw_mails`
|
||||
+ ` SET flags = (COALESCE(flags, 0) | ?) & ~?`
|
||||
+ ` SET flags = ${flagUpdate.expression}`
|
||||
+ ` WHERE id IN (${placeholders})`
|
||||
+ ` AND EXISTS (`
|
||||
+ `SELECT 1 FROM users_address ua`
|
||||
+ ` JOIN address a ON a.id = ua.address_id`
|
||||
+ ` WHERE ua.user_id = ? AND a.name = raw_mails.address`
|
||||
+ `)${condition}`
|
||||
).bind(
|
||||
...flagUpdate.params,
|
||||
...update.ids,
|
||||
user_id,
|
||||
...(flagUpdate.conditionParams ?? []),
|
||||
).run();
|
||||
if (!result.success) return c.json({ success: false, changes: 0, results: [] }, 500);
|
||||
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT id, flags FROM raw_mails`
|
||||
+ ` WHERE id IN (${placeholders})`
|
||||
+ ` AND EXISTS (`
|
||||
+ `SELECT 1 FROM users_address ua`
|
||||
+ ` JOIN address a ON a.id = ua.address_id`
|
||||
+ ` WHERE ua.user_id = ? AND a.name = raw_mails.address`
|
||||
+ `)`
|
||||
).bind(update.add, update.remove, ...update.ids, user_id).run();
|
||||
return c.json({ success: result.success, changes: result.meta.changes ?? 0 });
|
||||
).bind(...update.ids, user_id).all();
|
||||
return c.json({
|
||||
success: true,
|
||||
changes: result.meta.changes ?? 0,
|
||||
results: results.map(row => serializeMailFlags(row, true)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user