mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-04 23:17:34 +08:00
refactor: centralize mail flag handling
This commit is contained in:
+1
-1
@@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -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>) => {
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user