mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-01 21:46:45 +08:00
feat: add extensible mail flags
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { Context } from "hono";
|
||||
import { handleMailListQuery } from "../common";
|
||||
import { resolveRawEmailRow } from "../gzip";
|
||||
import { serializeMailFlags } from "../mail_flags";
|
||||
import { getBooleanValue } from "../utils";
|
||||
|
||||
export default {
|
||||
getMails: async (c: Context<HonoCustomType>) => {
|
||||
@@ -31,7 +33,8 @@ export default {
|
||||
`SELECT * FROM raw_mails WHERE id = ?`
|
||||
).bind(id).first();
|
||||
if (!result) return c.json(null);
|
||||
return c.json(await resolveRawEmailRow(result));
|
||||
const resolved = await resolveRawEmailRow(result);
|
||||
return c.json(serializeMailFlags(resolved, getBooleanValue(c.env.ENABLE_MAIL_FLAGS)));
|
||||
},
|
||||
deleteMail: async (c: Context<HonoCustomType>) => {
|
||||
const { id } = c.req.param();
|
||||
|
||||
@@ -11,6 +11,7 @@ CREATE TABLE IF NOT EXISTS raw_mails (
|
||||
raw TEXT,
|
||||
raw_blob BLOB,
|
||||
metadata TEXT,
|
||||
flags INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
@@ -197,6 +198,17 @@ export default {
|
||||
await c.env.DB.exec(`ALTER TABLE raw_mails ADD COLUMN raw_blob BLOB;`);
|
||||
}
|
||||
}
|
||||
if (version && version <= "v0.0.7") {
|
||||
const tableInfo = await c.env.DB.prepare(
|
||||
`PRAGMA table_info(raw_mails)`
|
||||
).all();
|
||||
const hasFlags = tableInfo.results?.some(
|
||||
(col: any) => col.name === 'flags'
|
||||
);
|
||||
if (!hasFlags) {
|
||||
await c.env.DB.exec(`ALTER TABLE raw_mails ADD COLUMN flags INTEGER;`);
|
||||
}
|
||||
}
|
||||
if (version != CONSTANTS.DB_VERSION) {
|
||||
// remove all \r and \n characters from the query string
|
||||
// split by ; and join with a ;\n
|
||||
|
||||
@@ -40,6 +40,7 @@ export default {
|
||||
"ENABLE_USER_CREATE_EMAIL": utils.getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL),
|
||||
"DISABLE_ANONYMOUS_USER_CREATE_EMAIL": utils.getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL),
|
||||
"ENABLE_USER_DELETE_EMAIL": utils.getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL),
|
||||
"ENABLE_MAIL_FLAGS": utils.getBooleanValue(c.env.ENABLE_MAIL_FLAGS),
|
||||
"ENABLE_AUTO_REPLY": utils.getBooleanValue(c.env.ENABLE_AUTO_REPLY),
|
||||
"COPYRIGHT": c.env.COPYRIGHT,
|
||||
"ENABLE_WEBHOOK": utils.getBooleanValue(c.env.ENABLE_WEBHOOK),
|
||||
|
||||
@@ -39,6 +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),
|
||||
"enableMailFlags": 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,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { unbindTelegramByAddress } from './telegram_api/common';
|
||||
import { CONSTANTS } from './constants';
|
||||
import { AddressCreationSettings, AdminWebhookSettings, ExtractResult, WebhookMail, WebhookSettings } from './models';
|
||||
import i18n from './i18n';
|
||||
import { serializeMailFlags } from './mail_flags';
|
||||
|
||||
const DEFAULT_NAME_REGEX = /[^a-z0-9]/g;
|
||||
const DEFAULT_RANDOM_SUBDOMAIN_LENGTH = 8;
|
||||
@@ -720,7 +721,9 @@ export const handleMailListQuery = async (
|
||||
const { results } = await c.env.DB.prepare(resultsQuery).bind(
|
||||
...params, limit, offset
|
||||
).all();
|
||||
const resolvedResults = await resolveRawEmailList(results);
|
||||
const resolvedResults = (await resolveRawEmailList(results)).map(row =>
|
||||
serializeMailFlags(row, getBooleanValue(c.env.ENABLE_MAIL_FLAGS))
|
||||
);
|
||||
const count = offset == 0 ? await c.env.DB.prepare(
|
||||
countQuery
|
||||
).bind(...params).first("count") : 0;
|
||||
|
||||
@@ -3,7 +3,7 @@ export const CONSTANTS = {
|
||||
|
||||
// DB Version
|
||||
DB_VERSION_KEY: 'db_version',
|
||||
DB_VERSION: "v0.0.7",
|
||||
DB_VERSION: "v0.0.8",
|
||||
|
||||
// DB settings
|
||||
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
||||
|
||||
+36
-20
@@ -12,6 +12,7 @@ import { forwardEmail } from "./forward";
|
||||
import { EmailRuleSettings } from "../models";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { compressText } from "../gzip";
|
||||
import { insertRawMail, resolveInitialMailFlags } from "../mail_flags";
|
||||
|
||||
|
||||
async function email(message: ForwardableEmailMessage, env: Bindings, ctx: ExecutionContext) {
|
||||
@@ -67,6 +68,9 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
|
||||
const message_id = message.headers.get("Message-ID");
|
||||
// save email
|
||||
try {
|
||||
const initialFlags = await resolveInitialMailFlags(
|
||||
getBooleanValue(env.ENABLE_MAIL_FLAGS), env, toAddress, parsedEmailContext
|
||||
);
|
||||
let success = false;
|
||||
if (getBooleanValue(env.ENABLE_MAIL_GZIP)) {
|
||||
let compressed: ArrayBuffer | null = null;
|
||||
@@ -77,38 +81,50 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
|
||||
}
|
||||
if (compressed) {
|
||||
try {
|
||||
({ success } = await env.DB.prepare(
|
||||
`INSERT INTO raw_mails (source, address, raw_blob, message_id) VALUES (?, ?, ?, ?)`
|
||||
).bind(
|
||||
message.from, toAddress, compressed, message_id
|
||||
).run());
|
||||
({ success } = await insertRawMail(env.DB, {
|
||||
source: message.from,
|
||||
address: toAddress,
|
||||
content: compressed,
|
||||
contentColumn: 'raw_blob',
|
||||
messageId: message_id,
|
||||
flags: initialFlags,
|
||||
}));
|
||||
} catch (dbError) {
|
||||
// Fallback to plaintext only if raw_blob column is missing (migration not applied)
|
||||
const errMsg = String(dbError);
|
||||
if (errMsg.includes('raw_blob') || errMsg.includes('no such column')) {
|
||||
console.error("raw_blob column missing, falling back to plaintext", dbError);
|
||||
({ success } = await env.DB.prepare(
|
||||
`INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
|
||||
).bind(
|
||||
message.from, toAddress, parsedEmailContext.rawEmail, message_id
|
||||
).run());
|
||||
({ success } = await insertRawMail(env.DB, {
|
||||
source: message.from,
|
||||
address: toAddress,
|
||||
content: parsedEmailContext.rawEmail,
|
||||
contentColumn: 'raw',
|
||||
messageId: message_id,
|
||||
flags: initialFlags,
|
||||
}));
|
||||
} else {
|
||||
throw dbError;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
({ success } = await env.DB.prepare(
|
||||
`INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
|
||||
).bind(
|
||||
message.from, toAddress, parsedEmailContext.rawEmail, message_id
|
||||
).run());
|
||||
({ success } = await insertRawMail(env.DB, {
|
||||
source: message.from,
|
||||
address: toAddress,
|
||||
content: parsedEmailContext.rawEmail,
|
||||
contentColumn: 'raw',
|
||||
messageId: message_id,
|
||||
flags: initialFlags,
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
({ success } = await env.DB.prepare(
|
||||
`INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
|
||||
).bind(
|
||||
message.from, toAddress, parsedEmailContext.rawEmail, message_id
|
||||
).run());
|
||||
({ success } = await insertRawMail(env.DB, {
|
||||
source: message.from,
|
||||
address: toAddress,
|
||||
content: parsedEmailContext.rawEmail,
|
||||
contentColumn: 'raw',
|
||||
messageId: message_id,
|
||||
flags: initialFlags,
|
||||
}));
|
||||
}
|
||||
if (!success) {
|
||||
message.setReject(`Failed save message to ${toAddress}`);
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
export const MAIL_FLAGS = {
|
||||
UNREAD: 1 << 0,
|
||||
ANSWERED: 1 << 1,
|
||||
FLAGGED: 1 << 2,
|
||||
DELETED: 1 << 3,
|
||||
DRAFT: 1 << 4,
|
||||
JUNK: 1 << 5,
|
||||
} as const;
|
||||
|
||||
export const CUSTOM_MAIL_FLAG_OFFSET = 10;
|
||||
export const CUSTOM_MAIL_FLAG_COUNT = 10;
|
||||
export const MUTABLE_MAIL_FLAGS = MAIL_FLAGS.UNREAD;
|
||||
|
||||
export const getCustomMailFlag = (slot: number): number => {
|
||||
if (!Number.isInteger(slot) || slot < 0 || slot >= CUSTOM_MAIL_FLAG_COUNT) {
|
||||
throw new Error("Invalid custom mail flag slot");
|
||||
}
|
||||
return 1 << (CUSTOM_MAIL_FLAG_OFFSET + slot);
|
||||
};
|
||||
|
||||
export const serializeMailFlags = <T extends Record<string, unknown>>(
|
||||
row: T,
|
||||
enabled: boolean,
|
||||
): T => {
|
||||
const result = { ...row };
|
||||
if (!enabled) {
|
||||
delete result.flags;
|
||||
return result;
|
||||
}
|
||||
result.flags = Number(result.flags ?? 0);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const resolveInitialMailFlags = async (
|
||||
enabled: boolean,
|
||||
_env: Bindings,
|
||||
_address: string,
|
||||
_parsedEmailContext: ParsedEmailContext,
|
||||
): Promise<number | null> => {
|
||||
if (!enabled) return null;
|
||||
return MAIL_FLAGS.UNREAD;
|
||||
};
|
||||
|
||||
type InsertRawMailParams = {
|
||||
source: string;
|
||||
address: string;
|
||||
content: string | ArrayBuffer;
|
||||
contentColumn: 'raw' | 'raw_blob';
|
||||
messageId: string | null;
|
||||
flags: number | null;
|
||||
};
|
||||
|
||||
export const insertRawMail = async (
|
||||
db: D1Database,
|
||||
params: InsertRawMailParams,
|
||||
) => {
|
||||
const { source, address, content, contentColumn, messageId, flags } = params;
|
||||
if (flags === null) {
|
||||
return db.prepare(
|
||||
`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 MailFlagUpdate = {
|
||||
ids: number[];
|
||||
add: number;
|
||||
remove: number;
|
||||
};
|
||||
|
||||
export type MailFlagFilter = {
|
||||
mask: number;
|
||||
state: 'set' | 'unset';
|
||||
};
|
||||
|
||||
export const parseMailFlagFilter = (
|
||||
bitValue: 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 (stateValue !== 'set' && stateValue !== 'unset') return null;
|
||||
return { mask: 1 << bit, state: stateValue };
|
||||
};
|
||||
|
||||
export const parseMailFlagUpdate = (value: unknown): MailFlagUpdate | 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;
|
||||
if (body.ids.some(id => typeof id !== 'number')) return 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;
|
||||
|
||||
return { ids, add, remove };
|
||||
};
|
||||
@@ -28,6 +28,7 @@ api.post('/api/attachment/get_url', s3_attachment.getSignedGetUrl)
|
||||
// mail crud
|
||||
api.get('/api/mails', mails_crud.listMails)
|
||||
api.get('/api/mail/:mail_id', mails_crud.getMail)
|
||||
api.patch('/api/mails/flags', mails_crud.updateMailFlags)
|
||||
api.delete('/api/mails/:id', mails_crud.deleteMail)
|
||||
|
||||
// parsed mail (server-side parsed subject/text/html/attachments)
|
||||
|
||||
@@ -5,18 +5,32 @@ 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';
|
||||
|
||||
const listMails = async (c: Context<HonoCustomType>) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
if (!address) {
|
||||
return c.json({ "error": "No address" }, 400)
|
||||
}
|
||||
const { limit, offset } = c.req.query();
|
||||
const { limit, offset, flag, flag_state } = c.req.query();
|
||||
if (Number.parseInt(offset) <= 0) updateAddressUpdatedAt(c, address);
|
||||
const flagFilter = parseMailFlagFilter(flag, flag_state);
|
||||
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);
|
||||
}
|
||||
|
||||
const filters = [`address = ?`];
|
||||
const params = [address];
|
||||
if (flagFilter) {
|
||||
filters.push(`(COALESCE(flags, 0) & ?) ${flagFilter.state === 'set' ? '!=' : '='} 0`);
|
||||
params.push(String(flagFilter.mask));
|
||||
}
|
||||
const whereClause = filters.join(' AND ');
|
||||
return await handleMailListQuery(c,
|
||||
`SELECT * FROM raw_mails where address = ?`,
|
||||
`SELECT count(*) as count FROM raw_mails where address = ?`,
|
||||
[address], limit, offset
|
||||
`SELECT * FROM raw_mails WHERE ${whereClause}`,
|
||||
`SELECT count(*) as count FROM raw_mails WHERE ${whereClause}`,
|
||||
params, limit, offset
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,7 +41,8 @@ const getMail = async (c: Context<HonoCustomType>) => {
|
||||
`SELECT * FROM raw_mails where id = ? and address = ?`
|
||||
).bind(mail_id, address).first();
|
||||
if (!result) return c.json(null);
|
||||
return c.json(await resolveRawEmailRow(result));
|
||||
const resolved = await resolveRawEmailRow(result);
|
||||
return c.json(serializeMailFlags(resolved, getBooleanValue(c.env.ENABLE_MAIL_FLAGS)));
|
||||
};
|
||||
|
||||
const deleteMail = async (c: Context<HonoCustomType>) => {
|
||||
@@ -44,6 +59,23 @@ const deleteMail = async (c: Context<HonoCustomType>) => {
|
||||
return c.json({ success });
|
||||
};
|
||||
|
||||
const updateMailFlags = async (c: Context<HonoCustomType>) => {
|
||||
if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
||||
return c.json({ error: "Mail flags are disabled" }, 403);
|
||||
}
|
||||
const update = parseMailFlagUpdate(await c.req.json().catch(() => null));
|
||||
if (!update) return c.json({ error: "Invalid mail flags request" }, 400);
|
||||
|
||||
const { address } = c.get("jwtPayload");
|
||||
const placeholders = update.ids.map(() => '?').join(',');
|
||||
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 });
|
||||
};
|
||||
|
||||
const getSettings = async (c: Context<HonoCustomType>) => {
|
||||
const { address, address_id } = c.get("jwtPayload")
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
@@ -117,4 +149,7 @@ const clearSentItems = async (c: Context<HonoCustomType>) => {
|
||||
return c.json({ success });
|
||||
};
|
||||
|
||||
export default { listMails, getMail, deleteMail, getSettings, deleteAddress, clearInbox, clearSentItems };
|
||||
export default {
|
||||
listMails, getMail, deleteMail, updateMailFlags,
|
||||
getSettings, deleteAddress, clearInbox, clearSentItems
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@ import { Context } from 'hono'
|
||||
|
||||
import { commonParseMail, handleMailListQuery, updateAddressUpdatedAt } from '../common'
|
||||
import { resolveRawEmailRow } from '../gzip'
|
||||
import { serializeMailFlags } from '../mail_flags';
|
||||
import { getBooleanValue } from '../utils';
|
||||
|
||||
const toParsedMailRow = async (row: Record<string, unknown>): Promise<Record<string, unknown>> => {
|
||||
const raw = typeof row.raw === 'string' ? row.raw : '';
|
||||
@@ -46,7 +48,8 @@ const getParsedMail = async (c: Context<HonoCustomType>) => {
|
||||
).bind(mail_id, address).first();
|
||||
if (!row) return c.json(null);
|
||||
const resolved = await resolveRawEmailRow(row);
|
||||
return c.json(await toParsedMailRow(resolved as Record<string, unknown>));
|
||||
const serialized = serializeMailFlags(resolved, getBooleanValue(c.env.ENABLE_MAIL_FLAGS));
|
||||
return c.json(await toParsedMailRow(serialized));
|
||||
};
|
||||
|
||||
export default { listParsedMails, getParsedMail };
|
||||
|
||||
@@ -213,6 +213,7 @@ export type RawMailRow = {
|
||||
raw?: string;
|
||||
raw_blob?: unknown;
|
||||
metadata?: string;
|
||||
flags?: number | null;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
@@ -117,6 +117,7 @@ type Bindings = {
|
||||
|
||||
// gzip compression for raw_mails
|
||||
ENABLE_MAIL_GZIP: string | boolean | undefined
|
||||
ENABLE_MAIL_FLAGS: string | boolean | undefined
|
||||
CLEANUP_BATCH_SIZE: string | number | undefined
|
||||
|
||||
// E2E testing
|
||||
|
||||
@@ -16,6 +16,7 @@ api.get('/user_api/settings', settings.settings);
|
||||
|
||||
// mail api
|
||||
api.get('/user_api/mails', user_mail_api.getMails);
|
||||
api.patch('/user_api/mails/flags', user_mail_api.updateMailFlags);
|
||||
api.delete('/user_api/mails/:id', user_mail_api.deleteMail);
|
||||
|
||||
// send mail api
|
||||
|
||||
@@ -2,17 +2,27 @@ import { Context } from "hono";
|
||||
import i18n from "../i18n";
|
||||
import { handleMailListQuery } from "../common";
|
||||
import { getBooleanValue } from "../utils";
|
||||
import { parseMailFlagFilter, parseMailFlagUpdate } from "../mail_flags";
|
||||
|
||||
export default {
|
||||
getMails: async (c: Context<HonoCustomType>) => {
|
||||
const { user_id } = c.get("userPayload");
|
||||
const { address, limit, offset } = c.req.query();
|
||||
const { address, limit, offset, flag, flag_state } = 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 (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);
|
||||
}
|
||||
if (flagFilter) {
|
||||
filterQuerys.push(`(COALESCE(rm.flags, 0) & ?) ${flagFilter.state === 'set' ? '!=' : '='} 0`);
|
||||
filterParams.push(String(flagFilter.mask));
|
||||
}
|
||||
const fromQuery = ` FROM users_address ua`
|
||||
+ ` JOIN address a ON a.id = ua.address_id`
|
||||
+ ` JOIN raw_mails rm ON rm.address = a.name`
|
||||
@@ -41,5 +51,26 @@ export default {
|
||||
return c.json({
|
||||
success: success
|
||||
})
|
||||
},
|
||||
updateMailFlags: async (c: Context<HonoCustomType>) => {
|
||||
if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
||||
return c.json({ error: "Mail flags are disabled" }, 403);
|
||||
}
|
||||
const update = parseMailFlagUpdate(await c.req.json().catch(() => null));
|
||||
if (!update) return c.json({ error: "Invalid mail flags request" }, 400);
|
||||
|
||||
const { user_id } = c.get("userPayload");
|
||||
const placeholders = update.ids.map(() => '?').join(',');
|
||||
const result = await c.env.DB.prepare(
|
||||
`UPDATE raw_mails`
|
||||
+ ` SET flags = (COALESCE(flags, 0) | ?) & ~?`
|
||||
+ ` 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 });
|
||||
}
|
||||
}
|
||||
|
||||
+37
-12
@@ -3,6 +3,7 @@ import { createMimeMessage } from "mimetext";
|
||||
import { UserSettings, RoleAddressConfig } from "./models";
|
||||
import { CONSTANTS } from "./constants";
|
||||
import { compressText } from "./gzip";
|
||||
import { insertRawMail, resolveInitialMailFlags } from "./mail_flags";
|
||||
|
||||
export const getJsonObjectValue = <T = any>(
|
||||
value: string | any
|
||||
@@ -371,6 +372,10 @@ export const sendAdminInternalMail = async (
|
||||
});
|
||||
const message_id = Math.random().toString(36).substring(2, 15);
|
||||
const rawText = msg.asRaw();
|
||||
const parsedEmailContext: ParsedEmailContext = { rawEmail: rawText };
|
||||
const initialFlags = await resolveInitialMailFlags(
|
||||
getBooleanValue(c.env.ENABLE_MAIL_FLAGS), c.env, toMail, parsedEmailContext
|
||||
);
|
||||
let success = false;
|
||||
if (getBooleanValue(c.env.ENABLE_MAIL_GZIP)) {
|
||||
let compressed: ArrayBuffer | null = null;
|
||||
@@ -381,29 +386,49 @@ export const sendAdminInternalMail = async (
|
||||
}
|
||||
if (compressed) {
|
||||
try {
|
||||
({ success } = await c.env.DB.prepare(
|
||||
`INSERT INTO raw_mails (source, address, raw_blob, message_id) VALUES (?, ?, ?, ?)`
|
||||
).bind("admin@internal", toMail, compressed, message_id).run());
|
||||
({ success } = await insertRawMail(c.env.DB, {
|
||||
source: "admin@internal",
|
||||
address: toMail,
|
||||
content: compressed,
|
||||
contentColumn: 'raw_blob',
|
||||
messageId: message_id,
|
||||
flags: initialFlags,
|
||||
}));
|
||||
} catch (dbError) {
|
||||
const errMsg = String(dbError);
|
||||
if (errMsg.includes('raw_blob') || errMsg.includes('no such column')) {
|
||||
console.error("raw_blob column missing, falling back to plaintext", dbError);
|
||||
({ success } = await c.env.DB.prepare(
|
||||
`INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
|
||||
).bind("admin@internal", toMail, rawText, message_id).run());
|
||||
({ success } = await insertRawMail(c.env.DB, {
|
||||
source: "admin@internal",
|
||||
address: toMail,
|
||||
content: rawText,
|
||||
contentColumn: 'raw',
|
||||
messageId: message_id,
|
||||
flags: initialFlags,
|
||||
}));
|
||||
} else {
|
||||
throw dbError;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
({ success } = await c.env.DB.prepare(
|
||||
`INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
|
||||
).bind("admin@internal", toMail, rawText, message_id).run());
|
||||
({ success } = await insertRawMail(c.env.DB, {
|
||||
source: "admin@internal",
|
||||
address: toMail,
|
||||
content: rawText,
|
||||
contentColumn: 'raw',
|
||||
messageId: message_id,
|
||||
flags: initialFlags,
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
({ success } = await c.env.DB.prepare(
|
||||
`INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
|
||||
).bind("admin@internal", toMail, rawText, message_id).run());
|
||||
({ success } = await insertRawMail(c.env.DB, {
|
||||
source: "admin@internal",
|
||||
address: toMail,
|
||||
content: rawText,
|
||||
contentColumn: 'raw',
|
||||
messageId: message_id,
|
||||
flags: initialFlags,
|
||||
}));
|
||||
}
|
||||
if (!success) {
|
||||
console.log(`Failed save message from admin@internal to ${toMail}`);
|
||||
|
||||
@@ -77,6 +77,8 @@ ENABLE_USER_CREATE_EMAIL = true
|
||||
# DISABLE_ANONYMOUS_USER_CREATE_EMAIL = true
|
||||
# Allow users to delete messages
|
||||
ENABLE_USER_DELETE_EMAIL = true
|
||||
# Enable per-message mail flags such as unread state. Run the database migration before enabling.
|
||||
# ENABLE_MAIL_FLAGS = true
|
||||
# Allow automatic replies to emails
|
||||
ENABLE_AUTO_REPLY = false
|
||||
# Allow webhook
|
||||
|
||||
Reference in New Issue
Block a user