refactor: centralize mail flag handling

This commit is contained in:
dreamhunter2333
2026-08-25 21:13:34 +08:00
parent b0e801995b
commit b5d8f64320
12 changed files with 194 additions and 206 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS raw_mails (
raw TEXT, raw TEXT,
raw_blob BLOB, raw_blob BLOB,
metadata TEXT, metadata TEXT,
flags INTEGER NOT NULL DEFAULT 0, flags INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );
+4 -3
View File
@@ -1,7 +1,6 @@
import { Context } from "hono"; import { Context } from "hono";
import { handleMailListQuery } from "../common"; import { handleMailListQuery } from "../common";
import { resolveRawEmailRow } from "../gzip"; import { resolveRawEmailRow } from "../gzip";
import { serializeMailState } from "../mail_flags";
import { getBooleanValue } from "../utils"; import { getBooleanValue } from "../utils";
export default { export default {
@@ -33,8 +32,10 @@ export default {
`SELECT * FROM raw_mails WHERE id = ?` `SELECT * FROM raw_mails WHERE id = ?`
).bind(id).first(); ).bind(id).first();
if (!result) return c.json(null); if (!result) return c.json(null);
const resolved = await resolveRawEmailRow(result); return c.json(await resolveRawEmailRow(
return c.json(serializeMailState(resolved, getBooleanValue(c.env.ENABLE_MAIL_FLAGS))); result,
getBooleanValue(c.env.ENABLE_MAIL_FLAGS),
));
}, },
deleteMail: async (c: Context<HonoCustomType>) => { deleteMail: async (c: Context<HonoCustomType>) => {
const { id } = c.req.param(); const { id } = c.req.param();
+1 -1
View File
@@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS raw_mails (
raw TEXT, raw TEXT,
raw_blob BLOB, raw_blob BLOB,
metadata TEXT, metadata TEXT,
flags INTEGER NOT NULL DEFAULT 0, flags INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );
+3 -3
View File
@@ -7,7 +7,6 @@ import { unbindTelegramByAddress } from './telegram_api/common';
import { CONSTANTS } from './constants'; import { CONSTANTS } from './constants';
import { AddressCreationSettings, AdminWebhookSettings, ExtractResult, WebhookMail, WebhookSettings } from './models'; import { AddressCreationSettings, AdminWebhookSettings, ExtractResult, WebhookMail, WebhookSettings } from './models';
import i18n from './i18n'; import i18n from './i18n';
import { serializeMailState } from './mail_flags';
const DEFAULT_NAME_REGEX = /[^a-z0-9]/g; const DEFAULT_NAME_REGEX = /[^a-z0-9]/g;
const DEFAULT_RANDOM_SUBDOMAIN_LENGTH = 8; const DEFAULT_RANDOM_SUBDOMAIN_LENGTH = 8;
@@ -721,8 +720,9 @@ export const handleMailListQuery = async (
const { results } = await c.env.DB.prepare(resultsQuery).bind( const { results } = await c.env.DB.prepare(resultsQuery).bind(
...params, limit, offset ...params, limit, offset
).all(); ).all();
const resolvedResults = (await resolveRawEmailList(results)).map(row => const resolvedResults = await resolveRawEmailList(
serializeMailState(row, getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) results,
getBooleanValue(c.env.ENABLE_MAIL_FLAGS),
); );
const count = offset == 0 ? await c.env.DB.prepare( const count = offset == 0 ? await c.env.DB.prepare(
countQuery countQuery
+35 -36
View File
@@ -12,7 +12,7 @@ import { forwardEmail } from "./forward";
import { EmailRuleSettings } from "../models"; import { EmailRuleSettings } from "../models";
import { CONSTANTS } from "../constants"; import { CONSTANTS } from "../constants";
import { compressText } from "../gzip"; import { compressText } from "../gzip";
import { insertRawMail, resolveInitialMailFlags } from "../mail_flags"; import { updateInitialMailFlags } from "../mail_flags";
async function email(message: ForwardableEmailMessage, env: Bindings, ctx: ExecutionContext) { async function email(message: ForwardableEmailMessage, env: Bindings, ctx: ExecutionContext) {
@@ -68,10 +68,8 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
const message_id = message.headers.get("Message-ID"); const message_id = message.headers.get("Message-ID");
// save email // save email
try { try {
const initialFlags = await resolveInitialMailFlags(
getBooleanValue(env.ENABLE_MAIL_FLAGS), env, toAddress, parsedEmailContext
);
let success = false; let success = false;
let insertResult: D1Result | null = null;
if (getBooleanValue(env.ENABLE_MAIL_GZIP)) { if (getBooleanValue(env.ENABLE_MAIL_GZIP)) {
let compressed: ArrayBuffer | null = null; let compressed: ArrayBuffer | null = null;
try { try {
@@ -81,54 +79,55 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
} }
if (compressed) { if (compressed) {
try { try {
({ success } = await insertRawMail(env.DB, { insertResult = await env.DB.prepare(
source: message.from, `INSERT INTO raw_mails (source, address, raw_blob, message_id) VALUES (?, ?, ?, ?)`
address: toAddress, ).bind(
content: compressed, message.from, toAddress, compressed, message_id
contentColumn: 'raw_blob', ).run();
messageId: message_id, ({ success } = insertResult);
flags: initialFlags,
}));
} catch (dbError) { } catch (dbError) {
// Fallback to plaintext only if raw_blob column is missing (migration not applied) // Fallback to plaintext only if raw_blob column is missing (migration not applied)
const errMsg = String(dbError); const errMsg = String(dbError);
if (errMsg.includes('raw_blob') || errMsg.includes('no such column')) { if (errMsg.includes('raw_blob') || errMsg.includes('no such column')) {
console.error("raw_blob column missing, falling back to plaintext", dbError); console.error("raw_blob column missing, falling back to plaintext", dbError);
({ success } = await insertRawMail(env.DB, { insertResult = await env.DB.prepare(
source: message.from, `INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
address: toAddress, ).bind(
content: parsedEmailContext.rawEmail, message.from, toAddress, parsedEmailContext.rawEmail, message_id
contentColumn: 'raw', ).run();
messageId: message_id, ({ success } = insertResult);
flags: initialFlags,
}));
} else { } else {
throw dbError; throw dbError;
} }
} }
} else { } else {
({ success } = await insertRawMail(env.DB, { insertResult = await env.DB.prepare(
source: message.from, `INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
address: toAddress, ).bind(
content: parsedEmailContext.rawEmail, message.from, toAddress, parsedEmailContext.rawEmail, message_id
contentColumn: 'raw', ).run();
messageId: message_id, ({ success } = insertResult);
flags: initialFlags,
}));
} }
} else { } else {
({ success } = await insertRawMail(env.DB, { insertResult = await env.DB.prepare(
source: message.from, `INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
address: toAddress, ).bind(
content: parsedEmailContext.rawEmail, message.from, toAddress, parsedEmailContext.rawEmail, message_id
contentColumn: 'raw', ).run();
messageId: message_id, ({ success } = insertResult);
flags: initialFlags,
}));
} }
if (!success) { if (!success) {
message.setReject(`Failed save message to ${toAddress}`); message.setReject(`Failed save message to ${toAddress}`);
console.error(`Failed save message from ${message.from} to ${toAddress}`); console.error(`Failed save message from ${message.from} to ${toAddress}`);
} else {
await updateInitialMailFlags(
env.DB,
getBooleanValue(env.ENABLE_MAIL_FLAGS),
insertResult?.meta.last_row_id ?? 0,
env,
toAddress,
parsedEmailContext,
);
} }
} }
catch (error) { catch (error) {
+11 -4
View File
@@ -4,6 +4,7 @@
*/ */
import { RawMailRow } from "./models"; import { RawMailRow } from "./models";
import { serializeMailState } from "./mail_flags";
export async function compressText(text: string): Promise<ArrayBuffer> { export async function compressText(text: string): Promise<ArrayBuffer> {
const stream = new Blob([text]).stream().pipeThrough(new CompressionStream('gzip')); const stream = new Blob([text]).stream().pipeThrough(new CompressionStream('gzip'));
@@ -34,15 +35,21 @@ export async function resolveRawEmail(row: RawMailRow): Promise<string> {
/** /**
* Resolve a single row: decompress raw_blob if present, strip raw_blob from result. * Resolve a single row: decompress raw_blob if present, strip raw_blob from result.
*/ */
export async function resolveRawEmailRow(row: RawMailRow): Promise<RawMailRow> { export async function resolveRawEmailRow(
row: RawMailRow,
enableReadStatus = false,
): Promise<RawMailRow> {
const raw = await resolveRawEmail(row); const raw = await resolveRawEmail(row);
const { raw_blob: _, ...rest } = row; const { raw_blob: _, ...rest } = row;
return { ...rest, raw }; return serializeMailState({ ...rest, raw }, enableReadStatus);
} }
/** /**
* Batch resolve raw emails for list queries using Promise.all. * Batch resolve raw emails for list queries using Promise.all.
*/ */
export async function resolveRawEmailList(rows: RawMailRow[]): Promise<RawMailRow[]> { export async function resolveRawEmailList(
return Promise.all(rows.map(row => resolveRawEmailRow(row))); rows: RawMailRow[],
enableReadStatus = false,
): Promise<RawMailRow[]> {
return Promise.all(rows.map(row => resolveRawEmailRow(row, enableReadStatus)));
} }
+63 -33
View File
@@ -33,38 +33,25 @@ export const serializeMailState = <T extends Record<string, unknown>>(
return result; return result;
}; };
export const resolveInitialMailFlags = async ( const resolveInitialMailFlags = async (
enabled: boolean,
_env: Bindings, _env: Bindings,
_address: string, _address: string,
_parsedEmailContext: ParsedEmailContext, _parsedEmailContext: ParsedEmailContext,
): Promise<number | null> => { ): Promise<number> => {
if (!enabled) return null;
return MAIL_FLAGS.UNREAD; return MAIL_FLAGS.UNREAD;
}; };
type InsertRawMailParams = { export const updateInitialMailFlags = async (
source: string;
address: string;
content: string | ArrayBuffer;
contentColumn: 'raw' | 'raw_blob';
messageId: string | null;
flags: number | null;
};
export const insertRawMail = async (
db: D1Database, db: D1Database,
params: InsertRawMailParams, enabled: boolean,
mailId: number,
env: Bindings,
address: string,
parsedEmailContext: ParsedEmailContext,
) => { ) => {
const { source, address, content, contentColumn, messageId, flags } = params; if (!enabled || !Number.isInteger(mailId) || mailId <= 0) return;
if (flags === null) { const flags = await resolveInitialMailFlags(env, address, parsedEmailContext);
return db.prepare( await db.prepare(`UPDATE raw_mails SET flags = ? WHERE id = ?`).bind(flags, mailId).run();
`INSERT INTO raw_mails (source, address, ${contentColumn}, message_id) VALUES (?, ?, ?, ?)`
).bind(source, address, content, messageId).run();
}
return db.prepare(
`INSERT INTO raw_mails (source, address, ${contentColumn}, message_id, flags) VALUES (?, ?, ?, ?, ?)`
).bind(source, address, content, messageId, flags).run();
}; };
export type MailReadStatusUpdate = { export type MailReadStatusUpdate = {
@@ -73,21 +60,26 @@ export type MailReadStatusUpdate = {
action: MailReadStatusAction; action: MailReadStatusAction;
}; };
export type MailReadStatusFilter = { export type MailReadStatusQuery = {
mask: number; clause: string;
state: 'set' | 'unset'; params: string[];
}; };
export const parseReadStatusFilter = ( export const getReadStatusQuery = (
value: string | undefined, value: string | undefined,
): MailReadStatusFilter | undefined | null => { column: 'flags' | 'rm.flags',
): MailReadStatusQuery | undefined | null => {
if (value === undefined || value === 'all') return undefined; if (value === undefined || value === 'all') return undefined;
if (value === 'unread') return { mask: MAIL_FLAGS.UNREAD, state: 'set' }; if (value === 'unread') {
if (value === 'read') return { mask: MAIL_FLAGS.UNREAD, state: 'unset' }; 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; return null;
}; };
export const parseMailReadStatusUpdate = (value: unknown): MailReadStatusUpdate | null => { const parseMailReadStatusUpdate = (value: unknown): MailReadStatusUpdate | null => {
if (!value || typeof value !== 'object') return null; if (!value || typeof value !== 'object') return null;
const body = value as Record<string, unknown>; const body = value as Record<string, unknown>;
if (!Array.isArray(body.ids) || body.ids.length === 0 || body.ids.length > 100) return null; if (!Array.isArray(body.ids) || body.ids.length === 0 || body.ids.length > 100) return null;
@@ -101,7 +93,7 @@ export const parseMailReadStatusUpdate = (value: unknown): MailReadStatusUpdate
return { ids, mask: MAIL_FLAGS.UNREAD, action: body.action }; return { ids, mask: MAIL_FLAGS.UNREAD, action: body.action };
}; };
export const getMailReadStatusUpdateExpression = ( const getMailReadStatusUpdateExpression = (
update: MailReadStatusUpdate, update: MailReadStatusUpdate,
column = 'flags', column = 'flags',
): { expression: string; params: number[]; condition?: string; conditionParams?: number[] } => { ): { expression: string; params: number[]; condition?: string; conditionParams?: number[] } => {
@@ -126,3 +118,41 @@ export const getMailReadStatusUpdateExpression = (
params: [update.mask, update.mask], params: [update.mask, update.mask],
}; };
}; };
type MailScope = {
clause: string;
params: (string | number)[];
};
export const applyMailReadStatusUpdate = async (
db: D1Database,
scope: MailScope,
value: unknown,
) => {
const update = parseMailReadStatusUpdate(value);
if (!update) return null;
const placeholders = update.ids.map(() => '?').join(',');
const statusUpdate = getMailReadStatusUpdateExpression(update);
const condition = statusUpdate.condition ? ` AND ${statusUpdate.condition}` : '';
const result = await db.prepare(
`UPDATE raw_mails SET flags = ${statusUpdate.expression}`
+ ` WHERE id IN (${placeholders}) AND (${scope.clause})${condition}`
).bind(
...statusUpdate.params,
...update.ids,
...scope.params,
...(statusUpdate.conditionParams ?? []),
).run();
if (!result.success) return { success: false, changes: 0, results: [] };
const { results } = await db.prepare(
`SELECT id, flags FROM raw_mails`
+ ` WHERE id IN (${placeholders}) AND (${scope.clause})`
).bind(...update.ids, ...scope.params).all();
return {
success: true,
changes: result.meta.changes ?? 0,
results: results.map(row => serializeMailState(row, true)),
};
};
+20 -38
View File
@@ -6,10 +6,8 @@ import { handleMailListQuery, deleteAddressWithData, updateAddressUpdatedAt } fr
import { resolveRawEmailRow } from '../gzip' import { resolveRawEmailRow } from '../gzip'
import { getSendBalanceState } from './send_balance'; import { getSendBalanceState } from './send_balance';
import { import {
getMailReadStatusUpdateExpression, getReadStatusQuery,
parseMailReadStatusUpdate, applyMailReadStatusUpdate,
parseReadStatusFilter,
serializeMailState,
} from '../mail_flags'; } from '../mail_flags';
const listMails = async (c: Context<HonoCustomType>) => { const listMails = async (c: Context<HonoCustomType>) => {
@@ -19,17 +17,17 @@ const listMails = async (c: Context<HonoCustomType>) => {
} }
const { limit, offset, read_status } = c.req.query(); const { limit, offset, read_status } = c.req.query();
if (Number.parseInt(offset) <= 0) updateAddressUpdatedAt(c, address); if (Number.parseInt(offset) <= 0) updateAddressUpdatedAt(c, address);
const readStatusFilter = parseReadStatusFilter(read_status); const readStatusQuery = getReadStatusQuery(read_status, 'flags');
if (readStatusFilter === null) return c.json({ error: "Invalid mail read status filter" }, 400); if (readStatusQuery === null) return c.json({ error: "Invalid mail read status filter" }, 400);
if (readStatusFilter && !getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) { if (readStatusQuery && !getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
return c.json({ error: "Mail read status is disabled" }, 403); return c.json({ error: "Mail read status is disabled" }, 403);
} }
const filters = [`address = ?`]; const filters = [`address = ?`];
const params = [address]; const params = [address];
if (readStatusFilter) { if (readStatusQuery) {
filters.push(`(COALESCE(flags, 0) & ?) ${readStatusFilter.state === 'set' ? '!=' : '='} 0`); filters.push(readStatusQuery.clause);
params.push(String(readStatusFilter.mask)); params.push(...readStatusQuery.params);
} }
const whereClause = filters.join(' AND '); const whereClause = filters.join(' AND ');
return await handleMailListQuery(c, return await handleMailListQuery(c,
@@ -46,8 +44,10 @@ const getMail = async (c: Context<HonoCustomType>) => {
`SELECT * FROM raw_mails where id = ? and address = ?` `SELECT * FROM raw_mails where id = ? and address = ?`
).bind(mail_id, address).first(); ).bind(mail_id, address).first();
if (!result) return c.json(null); if (!result) return c.json(null);
const resolved = await resolveRawEmailRow(result); return c.json(await resolveRawEmailRow(
return c.json(serializeMailState(resolved, getBooleanValue(c.env.ENABLE_MAIL_FLAGS))); result,
getBooleanValue(c.env.ENABLE_MAIL_FLAGS),
));
}; };
const deleteMail = async (c: Context<HonoCustomType>) => { const deleteMail = async (c: Context<HonoCustomType>) => {
@@ -68,33 +68,15 @@ const updateMailReadStatus = async (c: Context<HonoCustomType>) => {
if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) { if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
return c.json({ error: "Mail read status is disabled" }, 403); return c.json({ error: "Mail read status is disabled" }, 403);
} }
const update = parseMailReadStatusUpdate(await c.req.json().catch(() => null));
if (!update) return c.json({ error: "Invalid mail read status request" }, 400);
const { address } = c.get("jwtPayload"); const { address } = c.get("jwtPayload");
const placeholders = update.ids.map(() => '?').join(','); const result = await applyMailReadStatusUpdate(
const statusUpdate = getMailReadStatusUpdateExpression(update); c.env.DB,
const condition = statusUpdate.condition ? ` AND ${statusUpdate.condition}` : ''; { clause: 'address = ?', params: [address] },
const result = await c.env.DB.prepare( await c.req.json().catch(() => null),
`UPDATE raw_mails` );
+ ` SET flags = ${statusUpdate.expression}` if (!result) return c.json({ error: "Invalid mail read status request" }, 400);
+ ` WHERE address = ? AND id IN (${placeholders})${condition}` if (!result.success) return c.json(result, 500);
).bind( return c.json(result);
...statusUpdate.params,
address,
...update.ids,
...(statusUpdate.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 => serializeMailState(row, true)),
});
}; };
const getSettings = async (c: Context<HonoCustomType>) => { const getSettings = async (c: Context<HonoCustomType>) => {
+5 -4
View File
@@ -2,7 +2,6 @@ import { Context } from 'hono'
import { commonParseMail, handleMailListQuery, updateAddressUpdatedAt } from '../common' import { commonParseMail, handleMailListQuery, updateAddressUpdatedAt } from '../common'
import { resolveRawEmailRow } from '../gzip' import { resolveRawEmailRow } from '../gzip'
import { serializeMailState } from '../mail_flags';
import { getBooleanValue } from '../utils'; import { getBooleanValue } from '../utils';
const toParsedMailRow = async (row: Record<string, unknown>): Promise<Record<string, unknown>> => { const toParsedMailRow = async (row: Record<string, unknown>): Promise<Record<string, unknown>> => {
@@ -47,9 +46,11 @@ const getParsedMail = async (c: Context<HonoCustomType>) => {
`SELECT * FROM raw_mails where id = ? and address = ?` `SELECT * FROM raw_mails where id = ? and address = ?`
).bind(mail_id, address).first(); ).bind(mail_id, address).first();
if (!row) return c.json(null); if (!row) return c.json(null);
const resolved = await resolveRawEmailRow(row); const resolved = await resolveRawEmailRow(
const serialized = serializeMailState(resolved, getBooleanValue(c.env.ENABLE_MAIL_FLAGS)); row,
return c.json(await toParsedMailRow(serialized)); getBooleanValue(c.env.ENABLE_MAIL_FLAGS),
);
return c.json(await toParsedMailRow(resolved));
}; };
export default { listParsedMails, getParsedMail }; export default { listParsedMails, getParsedMail };
+1
View File
@@ -214,6 +214,7 @@ export type RawMailRow = {
raw_blob?: unknown; raw_blob?: unknown;
metadata?: string; metadata?: string;
flags?: number | null; flags?: number | null;
unread?: boolean;
created_at?: string; created_at?: string;
} }
+23 -47
View File
@@ -3,10 +3,8 @@ import i18n from "../i18n";
import { handleMailListQuery } from "../common"; import { handleMailListQuery } from "../common";
import { getBooleanValue } from "../utils"; import { getBooleanValue } from "../utils";
import { import {
getMailReadStatusUpdateExpression, getReadStatusQuery,
parseMailReadStatusUpdate, applyMailReadStatusUpdate,
parseReadStatusFilter,
serializeMailState,
} from "../mail_flags"; } from "../mail_flags";
export default { export default {
@@ -19,14 +17,14 @@ export default {
filterQuerys.push(`rm.address = ?`); filterQuerys.push(`rm.address = ?`);
filterParams.push(address); filterParams.push(address);
} }
const readStatusFilter = parseReadStatusFilter(read_status); const readStatusQuery = getReadStatusQuery(read_status, 'rm.flags');
if (readStatusFilter === null) return c.json({ error: "Invalid mail read status filter" }, 400); if (readStatusQuery === null) return c.json({ error: "Invalid mail read status filter" }, 400);
if (readStatusFilter && !getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) { if (readStatusQuery && !getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
return c.json({ error: "Mail read status is disabled" }, 403); return c.json({ error: "Mail read status is disabled" }, 403);
} }
if (readStatusFilter) { if (readStatusQuery) {
filterQuerys.push(`(COALESCE(rm.flags, 0) & ?) ${readStatusFilter.state === 'set' ? '!=' : '='} 0`); filterQuerys.push(readStatusQuery.clause);
filterParams.push(String(readStatusFilter.mask)); filterParams.push(...readStatusQuery.params);
} }
const fromQuery = ` FROM users_address ua` const fromQuery = ` FROM users_address ua`
+ ` JOIN address a ON a.id = ua.address_id` + ` JOIN address a ON a.id = ua.address_id`
@@ -61,43 +59,21 @@ export default {
if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) { if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
return c.json({ error: "Mail read status is disabled" }, 403); return c.json({ error: "Mail read status is disabled" }, 403);
} }
const update = parseMailReadStatusUpdate(await c.req.json().catch(() => null));
if (!update) return c.json({ error: "Invalid mail read status request" }, 400);
const { user_id } = c.get("userPayload"); const { user_id } = c.get("userPayload");
const placeholders = update.ids.map(() => '?').join(','); const result = await applyMailReadStatusUpdate(
const statusUpdate = getMailReadStatusUpdateExpression(update); c.env.DB,
const condition = statusUpdate.condition ? ` AND ${statusUpdate.condition}` : ''; {
const result = await c.env.DB.prepare( clause: `EXISTS (`
`UPDATE raw_mails` + `SELECT 1 FROM users_address ua`
+ ` SET flags = ${statusUpdate.expression}` + ` JOIN address a ON a.id = ua.address_id`
+ ` WHERE id IN (${placeholders})` + ` WHERE ua.user_id = ? AND a.name = raw_mails.address`
+ ` AND EXISTS (` + `)`,
+ `SELECT 1 FROM users_address ua` params: [user_id],
+ ` JOIN address a ON a.id = ua.address_id` },
+ ` WHERE ua.user_id = ? AND a.name = raw_mails.address` await c.req.json().catch(() => null),
+ `)${condition}` );
).bind( if (!result) return c.json({ error: "Invalid mail read status request" }, 400);
...statusUpdate.params, if (!result.success) return c.json(result, 500);
...update.ids, return c.json(result);
user_id,
...(statusUpdate.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.ids, user_id).all();
return c.json({
success: true,
changes: result.meta.changes ?? 0,
results: results.map(row => serializeMailState(row, true)),
});
} }
} }
+27 -36
View File
@@ -3,7 +3,7 @@ import { createMimeMessage } from "mimetext";
import { UserSettings, RoleAddressConfig } from "./models"; import { UserSettings, RoleAddressConfig } from "./models";
import { CONSTANTS } from "./constants"; import { CONSTANTS } from "./constants";
import { compressText } from "./gzip"; import { compressText } from "./gzip";
import { insertRawMail, resolveInitialMailFlags } from "./mail_flags"; import { updateInitialMailFlags } from "./mail_flags";
export const getJsonObjectValue = <T = any>( export const getJsonObjectValue = <T = any>(
value: string | any value: string | any
@@ -373,10 +373,8 @@ export const sendAdminInternalMail = async (
const message_id = Math.random().toString(36).substring(2, 15); const message_id = Math.random().toString(36).substring(2, 15);
const rawText = msg.asRaw(); const rawText = msg.asRaw();
const parsedEmailContext: ParsedEmailContext = { rawEmail: rawText }; const parsedEmailContext: ParsedEmailContext = { rawEmail: rawText };
const initialFlags = await resolveInitialMailFlags(
getBooleanValue(c.env.ENABLE_MAIL_FLAGS), c.env, toMail, parsedEmailContext
);
let success = false; let success = false;
let insertResult: D1Result | null = null;
if (getBooleanValue(c.env.ENABLE_MAIL_GZIP)) { if (getBooleanValue(c.env.ENABLE_MAIL_GZIP)) {
let compressed: ArrayBuffer | null = null; let compressed: ArrayBuffer | null = null;
try { try {
@@ -386,52 +384,45 @@ export const sendAdminInternalMail = async (
} }
if (compressed) { if (compressed) {
try { try {
({ success } = await insertRawMail(c.env.DB, { insertResult = await c.env.DB.prepare(
source: "admin@internal", `INSERT INTO raw_mails (source, address, raw_blob, message_id) VALUES (?, ?, ?, ?)`
address: toMail, ).bind("admin@internal", toMail, compressed, message_id).run();
content: compressed, ({ success } = insertResult);
contentColumn: 'raw_blob',
messageId: message_id,
flags: initialFlags,
}));
} catch (dbError) { } catch (dbError) {
const errMsg = String(dbError); const errMsg = String(dbError);
if (errMsg.includes('raw_blob') || errMsg.includes('no such column')) { if (errMsg.includes('raw_blob') || errMsg.includes('no such column')) {
console.error("raw_blob column missing, falling back to plaintext", dbError); console.error("raw_blob column missing, falling back to plaintext", dbError);
({ success } = await insertRawMail(c.env.DB, { insertResult = await c.env.DB.prepare(
source: "admin@internal", `INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
address: toMail, ).bind("admin@internal", toMail, rawText, message_id).run();
content: rawText, ({ success } = insertResult);
contentColumn: 'raw',
messageId: message_id,
flags: initialFlags,
}));
} else { } else {
throw dbError; throw dbError;
} }
} }
} else { } else {
({ success } = await insertRawMail(c.env.DB, { insertResult = await c.env.DB.prepare(
source: "admin@internal", `INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
address: toMail, ).bind("admin@internal", toMail, rawText, message_id).run();
content: rawText, ({ success } = insertResult);
contentColumn: 'raw',
messageId: message_id,
flags: initialFlags,
}));
} }
} else { } else {
({ success } = await insertRawMail(c.env.DB, { insertResult = await c.env.DB.prepare(
source: "admin@internal", `INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
address: toMail, ).bind("admin@internal", toMail, rawText, message_id).run();
content: rawText, ({ success } = insertResult);
contentColumn: 'raw',
messageId: message_id,
flags: initialFlags,
}));
} }
if (!success) { if (!success) {
console.log(`Failed save message from admin@internal to ${toMail}`); console.log(`Failed save message from admin@internal to ${toMail}`);
} else {
await updateInitialMailFlags(
c.env.DB,
getBooleanValue(c.env.ENABLE_MAIL_FLAGS),
insertResult?.meta.last_row_id ?? 0,
c.env,
toMail,
parsedEmailContext,
);
} }
return success; return success;
} catch (error) { } catch (error) {