mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-06-26 09:52:38 +08:00
- Add isSendMailEnabled and isAnySendMailEnabled functions in common.ts - Return enableSendMail field in /open_api/settings - Hide sendmail tab, sendbox tab, and reply button when send mail is not configured - Check RESEND_TOKEN, SMTP_CONFIG, and SEND_MAIL binding per domain Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
668 lines
23 KiB
TypeScript
668 lines
23 KiB
TypeScript
import { Context } from 'hono';
|
|
import { Jwt } from 'hono/utils/jwt'
|
|
import { WorkerMailerOptions } from 'worker-mailer';
|
|
|
|
import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList, hashPassword, getJsonObjectValue } from './utils';
|
|
import { unbindTelegramByAddress } from './telegram_api/common';
|
|
import { CONSTANTS } from './constants';
|
|
import { AdminWebhookSettings, WebhookMail, WebhookSettings } from './models';
|
|
import i18n from './i18n';
|
|
|
|
const DEFAULT_NAME_REGEX = /[^a-z0-9]/g;
|
|
|
|
/**
|
|
* Check if send mail is enabled for a specific domain
|
|
*/
|
|
export const isSendMailEnabled = (
|
|
c: Context<HonoCustomType>,
|
|
mailDomain: string
|
|
): boolean => {
|
|
// Check resend token for domain or global
|
|
const resendEnabled = c.env.RESEND_TOKEN || c.env[
|
|
`RESEND_TOKEN_${mailDomain.replace(/\./g, "_").toUpperCase()}`
|
|
];
|
|
if (resendEnabled) return true;
|
|
|
|
// Check SMTP config for domain
|
|
const smtpConfigMap = getJsonObjectValue<Record<string, WorkerMailerOptions>>(c.env.SMTP_CONFIG);
|
|
if (smtpConfigMap && smtpConfigMap[mailDomain]) return true;
|
|
|
|
// Check SEND_MAIL binding
|
|
if (c.env.SEND_MAIL) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if send mail is enabled for any configured domain
|
|
*/
|
|
export const isAnySendMailEnabled = (c: Context<HonoCustomType>): boolean => {
|
|
const domains = getDomains(c);
|
|
return domains.some(domain => isSendMailEnabled(c, domain));
|
|
}
|
|
|
|
export const generateRandomName = (c: Context<HonoCustomType>): string => {
|
|
// name min length min 1
|
|
const minLength = Math.max(
|
|
getIntValue(c.env.MIN_ADDRESS_LEN, 1),
|
|
1
|
|
);
|
|
// name max length min 1
|
|
const maxLength = Math.max(
|
|
getIntValue(c.env.MAX_ADDRESS_LEN, 30),
|
|
1
|
|
);
|
|
|
|
// Build full name recursively until minimum length is reached
|
|
const buildName = (currentName: string = ""): string => {
|
|
return currentName.length >= minLength
|
|
? currentName
|
|
: buildName(currentName + Math.random().toString(36).substring(2, 15));
|
|
};
|
|
|
|
const fullName = buildName();
|
|
|
|
// Return truncated to max length
|
|
return fullName.substring(0, Math.min(fullName.length, maxLength));
|
|
};
|
|
|
|
const checkNameRegex = (c: Context<HonoCustomType>, name: string) => {
|
|
let error = null;
|
|
try {
|
|
const regexStr = getStringValue(c.env.ADDRESS_CHECK_REGEX);
|
|
if (!regexStr) return;
|
|
const regex = new RegExp(regexStr);
|
|
if (!regex.test(name)) {
|
|
error = new Error(`Name not match regex: /${regexStr}/`);
|
|
}
|
|
}
|
|
catch (e) {
|
|
console.error("Failed to check address regex", e);
|
|
}
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const getNameRegex = (c: Context<HonoCustomType>): RegExp => {
|
|
try {
|
|
const regex = getStringValue(c.env.ADDRESS_REGEX);
|
|
if (!regex) {
|
|
return DEFAULT_NAME_REGEX;
|
|
}
|
|
return new RegExp(regex, 'g');
|
|
}
|
|
catch (e) {
|
|
console.error("Failed to get address regex", e);
|
|
}
|
|
return DEFAULT_NAME_REGEX;
|
|
}
|
|
|
|
export function updateAddressUpdatedAt(
|
|
c: Context<HonoCustomType>,
|
|
address: string | undefined | null
|
|
): void {
|
|
if (!address) {
|
|
return;
|
|
}
|
|
// update address updated_at asynchronously
|
|
c.executionCtx.waitUntil((async () => {
|
|
try {
|
|
await c.env.DB.prepare(
|
|
`UPDATE address SET updated_at = datetime('now') where name = ?`
|
|
).bind(address).run();
|
|
} catch (e) {
|
|
console.warn("[updateAddressUpdatedAt] failed:", address, e);
|
|
}
|
|
})());
|
|
}
|
|
|
|
export const generateRandomPassword = (): string => {
|
|
const charset = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
let password = "";
|
|
for (let i = 0; i < 8; i++) {
|
|
password += charset.charAt(Math.floor(Math.random() * charset.length));
|
|
}
|
|
return password;
|
|
}
|
|
|
|
const generatePasswordForAddress = async (
|
|
c: Context<HonoCustomType>,
|
|
address: string
|
|
): Promise<string | null> => {
|
|
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
|
|
return null;
|
|
}
|
|
|
|
const plainPassword = generateRandomPassword();
|
|
const hashedPassword = await hashPassword(plainPassword);
|
|
const { success } = await c.env.DB.prepare(
|
|
`UPDATE address SET password = ?, updated_at = datetime('now') WHERE name = ?`
|
|
).bind(hashedPassword, address).run();
|
|
|
|
if (!success) {
|
|
console.warn("Failed to set generated password for address:", address);
|
|
return null;
|
|
}
|
|
|
|
return plainPassword;
|
|
}
|
|
|
|
export const newAddress = async (
|
|
c: Context<HonoCustomType>,
|
|
{
|
|
name,
|
|
domain,
|
|
enablePrefix,
|
|
checkLengthByConfig = true,
|
|
addressPrefix = null,
|
|
checkAllowDomains = true,
|
|
enableCheckNameRegex = true,
|
|
sourceMeta = null,
|
|
}: {
|
|
name: string, domain: string | undefined | null,
|
|
enablePrefix: boolean,
|
|
checkLengthByConfig?: boolean,
|
|
addressPrefix?: string | undefined | null,
|
|
checkAllowDomains?: boolean,
|
|
enableCheckNameRegex?: boolean,
|
|
sourceMeta?: string | undefined | null,
|
|
}
|
|
): Promise<{ address: string, jwt: string, password?: string | null }> => {
|
|
const msgs = i18n.getMessagesbyContext(c);
|
|
// trim whitespace and remove special characters
|
|
name = name.trim().replace(getNameRegex(c), '')
|
|
// check name
|
|
if (enableCheckNameRegex) {
|
|
await checkNameBlockList(c, name);
|
|
checkNameRegex(c, name);
|
|
}
|
|
// name min length min 1
|
|
const minAddressLength = Math.max(
|
|
checkLengthByConfig ? getIntValue(c.env.MIN_ADDRESS_LEN, 1) : 1,
|
|
1
|
|
);
|
|
// name max length min 1
|
|
const maxAddressLength = Math.max(
|
|
checkLengthByConfig ? getIntValue(c.env.MAX_ADDRESS_LEN, 30) : 30,
|
|
1
|
|
);
|
|
// check name length
|
|
if (name.length < minAddressLength) {
|
|
throw new Error(`${msgs.NameTooShortMsg} (min ${minAddressLength})`);
|
|
}
|
|
if (name.length > maxAddressLength) {
|
|
throw new Error(`${msgs.NameTooLongMsg} (max ${maxAddressLength})`);
|
|
}
|
|
// create address with prefix
|
|
if (typeof addressPrefix === "string") {
|
|
name = addressPrefix.trim() + name;
|
|
} else if (enablePrefix) {
|
|
name = getStringValue(c.env.PREFIX).trim() + name;
|
|
}
|
|
// check domain
|
|
const allowDomains = checkAllowDomains ? await getAllowDomains(c) : getDomains(c);
|
|
// if domain is not set, select domain based on environment configuration
|
|
if (!domain && allowDomains.length > 0) {
|
|
const createAddressDefaultDomainFirst = getBooleanValue(c.env.CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST);
|
|
if (createAddressDefaultDomainFirst) {
|
|
domain = allowDomains[0];
|
|
} else {
|
|
domain = allowDomains[Math.floor(Math.random() * allowDomains.length)];
|
|
}
|
|
}
|
|
// check domain is valid
|
|
if (!domain || !allowDomains.includes(domain)) {
|
|
throw new Error(msgs.InvalidDomainMsg)
|
|
}
|
|
// create address
|
|
name = name + "@" + domain;
|
|
try {
|
|
// Try insert with source_meta field first
|
|
const result = await c.env.DB.prepare(
|
|
`INSERT INTO address(name, source_meta) VALUES(?, ?)`
|
|
).bind(name, sourceMeta).run();
|
|
if (!result.success) {
|
|
throw new Error(msgs.FailedCreateAddressMsg)
|
|
}
|
|
await updateAddressUpdatedAt(c, name);
|
|
} catch (e) {
|
|
const message = (e as Error).message;
|
|
// Fallback: source_meta field may not exist, try without it
|
|
if (message && message.includes("source_meta")) {
|
|
const result = await c.env.DB.prepare(
|
|
`INSERT INTO address(name) VALUES(?)`
|
|
).bind(name).run();
|
|
if (!result.success) {
|
|
throw new Error(msgs.FailedCreateAddressMsg)
|
|
}
|
|
await updateAddressUpdatedAt(c, name);
|
|
} else if (message && message.includes("UNIQUE")) {
|
|
throw new Error(msgs.AddressAlreadyExistsMsg)
|
|
} else {
|
|
throw new Error(msgs.FailedCreateAddressMsg)
|
|
}
|
|
}
|
|
const address_id = await c.env.DB.prepare(
|
|
`SELECT id FROM address where name = ?`
|
|
).bind(name).first<number>("id");
|
|
|
|
// 如果启用地址密码功能,自动生成密码
|
|
const generatedPassword = await generatePasswordForAddress(c, name);
|
|
|
|
// create jwt
|
|
const jwt = await Jwt.sign({
|
|
address: name,
|
|
address_id: address_id
|
|
}, c.env.JWT_SECRET, "HS256")
|
|
return {
|
|
jwt: jwt,
|
|
address: name,
|
|
password: generatedPassword,
|
|
}
|
|
}
|
|
|
|
const checkNameBlockList = async (
|
|
c: Context<HonoCustomType>, name: string
|
|
): Promise<void> => {
|
|
// check name block list
|
|
const blockList = [] as string[];
|
|
try {
|
|
const value = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY);
|
|
blockList.push(...(value || []));
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
if (blockList.some((item) => name.includes(item))) {
|
|
throw new Error(`Name[${name}]is blocked`);
|
|
}
|
|
}
|
|
|
|
export const cleanup = async (
|
|
c: Context<HonoCustomType>,
|
|
cleanType: string | undefined | null,
|
|
cleanDays: number | undefined | null
|
|
): Promise<boolean> => {
|
|
const msgs = i18n.getMessagesbyContext(c);
|
|
if (!cleanType || typeof cleanDays !== 'number' || cleanDays < 0 || cleanDays > 1000) {
|
|
throw new Error(msgs.InvalidCleanupConfigMsg)
|
|
}
|
|
console.log(`Cleanup ${cleanType} before ${cleanDays} days`);
|
|
switch (cleanType) {
|
|
case "inactiveAddress":
|
|
await batchDeleteAddressWithData(
|
|
c,
|
|
`updated_at < datetime('now', '-${cleanDays} day')`
|
|
)
|
|
break;
|
|
case "addressCreated":
|
|
await batchDeleteAddressWithData(
|
|
c,
|
|
`created_at < datetime('now', '-${cleanDays} day')`
|
|
)
|
|
break;
|
|
case "unboundAddress":
|
|
await batchDeleteAddressWithData(
|
|
c,
|
|
`id NOT IN (SELECT address_id FROM users_address) AND created_at < datetime('now', '-${cleanDays} day')`
|
|
)
|
|
break;
|
|
case "mails":
|
|
await c.env.DB.prepare(`
|
|
DELETE FROM raw_mails WHERE created_at < datetime('now', '-${cleanDays} day')`
|
|
).run();
|
|
break;
|
|
case "mails_unknow":
|
|
await c.env.DB.prepare(`
|
|
DELETE FROM raw_mails WHERE address NOT IN
|
|
(select name from address) AND created_at < datetime('now', '-${cleanDays} day')`
|
|
).run();
|
|
break;
|
|
case "sendbox":
|
|
await c.env.DB.prepare(`
|
|
DELETE FROM sendbox WHERE created_at < datetime('now', '-${cleanDays} day')`
|
|
).run();
|
|
break;
|
|
case "emptyAddress":
|
|
// Delete addresses that have no emails and were created more than N days ago
|
|
await batchDeleteAddressWithData(
|
|
c,
|
|
`name NOT IN (SELECT DISTINCT address FROM raw_mails WHERE address IS NOT NULL) AND created_at < datetime('now', '-${cleanDays} day')`
|
|
)
|
|
break;
|
|
default:
|
|
throw new Error(msgs.InvalidCleanTypeMsg)
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const batchDeleteAddressWithData = async (
|
|
c: Context<HonoCustomType>,
|
|
addressQueryCondition: string,
|
|
): Promise<boolean> => {
|
|
await c.env.DB.prepare(
|
|
`DELETE FROM raw_mails WHERE address IN ( ` +
|
|
`SELECT name FROM address WHERE ${addressQueryCondition})`
|
|
).run();
|
|
await c.env.DB.prepare(
|
|
`DELETE FROM sendbox WHERE address IN ( ` +
|
|
`SELECT name FROM address WHERE ${addressQueryCondition})`
|
|
).run();
|
|
await c.env.DB.prepare(
|
|
`DELETE FROM auto_reply_mails WHERE address IN ( ` +
|
|
`SELECT name FROM address WHERE ${addressQueryCondition})`
|
|
).run();
|
|
await c.env.DB.prepare(
|
|
`DELETE FROM address_sender WHERE address IN ( ` +
|
|
`SELECT name FROM address WHERE ${addressQueryCondition})`
|
|
).run();
|
|
await c.env.DB.prepare(
|
|
`DELETE FROM users_address WHERE address_id IN ( ` +
|
|
`SELECT id FROM address WHERE ${addressQueryCondition})`
|
|
).run();
|
|
// delete address
|
|
await c.env.DB.prepare(`
|
|
DELETE FROM address WHERE ${addressQueryCondition}`
|
|
).run();
|
|
return true;
|
|
}
|
|
|
|
|
|
export const deleteAddressWithData = async (
|
|
c: Context<HonoCustomType>,
|
|
address: string | undefined | null,
|
|
address_id: number | undefined | null
|
|
): Promise<boolean> => {
|
|
const msgs = i18n.getMessagesbyContext(c);
|
|
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
|
throw new Error(msgs.UserDeleteEmailDisabledMsg)
|
|
}
|
|
if (!address && !address_id) {
|
|
throw new Error(msgs.RequiredFieldMsg)
|
|
}
|
|
// get address_id or address
|
|
if (!address_id) {
|
|
address_id = await c.env.DB.prepare(
|
|
`SELECT id FROM address where name = ?`
|
|
).bind(address).first<number>("id");
|
|
} else if (!address) {
|
|
address = await c.env.DB.prepare(
|
|
`SELECT name FROM address where id = ?`
|
|
).bind(address_id).first<string>("name");
|
|
}
|
|
// check address again
|
|
if (!address || !address_id) {
|
|
throw new Error(msgs.AddressNotFoundMsg);
|
|
}
|
|
// unbind telegram
|
|
await unbindTelegramByAddress(c, address);
|
|
// delete address and related data
|
|
const { success: mailSuccess } = await c.env.DB.prepare(
|
|
`DELETE FROM raw_mails WHERE address = ? `
|
|
).bind(address).run();
|
|
const { success: sendAccess } = await c.env.DB.prepare(
|
|
`DELETE FROM address_sender WHERE address = ? `
|
|
).bind(address).run();
|
|
const { success: sendboxSuccess } = await c.env.DB.prepare(
|
|
`DELETE FROM sendbox WHERE address = ? `
|
|
).bind(address).run();
|
|
const { success: addressSuccess } = await c.env.DB.prepare(
|
|
`DELETE FROM users_address WHERE address_id = ? `
|
|
).bind(address_id).run();
|
|
const { success: autoReplySuccess } = await c.env.DB.prepare(
|
|
`DELETE FROM auto_reply_mails WHERE address = ? `
|
|
).bind(address).run();
|
|
const { success } = await c.env.DB.prepare(
|
|
`DELETE FROM address WHERE name = ? `
|
|
).bind(address).run();
|
|
if (!success || !mailSuccess || !sendboxSuccess || !addressSuccess || !sendAccess || !autoReplySuccess) {
|
|
throw new Error(msgs.OperationFailedMsg)
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export const handleListQuery = async (
|
|
c: Context<HonoCustomType>,
|
|
query: string, countQuery: string, params: string[],
|
|
limit: string | number | undefined | null,
|
|
offset: string | number | undefined | null
|
|
): Promise<Response> => {
|
|
const msgs = i18n.getMessagesbyContext(c);
|
|
if (typeof limit === "string") {
|
|
limit = parseInt(limit);
|
|
}
|
|
if (typeof offset === "string") {
|
|
offset = parseInt(offset);
|
|
}
|
|
if (!limit || limit < 0 || limit > 100) {
|
|
return c.text(msgs.InvalidLimitMsg, 400)
|
|
}
|
|
if (offset == null || offset == undefined || offset < 0) {
|
|
return c.text(msgs.InvalidOffsetMsg, 400)
|
|
}
|
|
const resultsQuery = `${query} order by id desc limit ? offset ?`;
|
|
const { results } = await c.env.DB.prepare(resultsQuery).bind(
|
|
...params, limit, offset
|
|
).all();
|
|
const count = offset == 0 ? await c.env.DB.prepare(
|
|
countQuery
|
|
).bind(...params).first("count") : 0;
|
|
return c.json({ results, count });
|
|
}
|
|
|
|
|
|
export const commonParseMail = async (parsedEmailContext: ParsedEmailContext): Promise<{
|
|
sender: string,
|
|
subject: string,
|
|
text: string,
|
|
html: string,
|
|
headers?: Record<string, string>[]
|
|
} | undefined> => {
|
|
// check parsed email context is valid
|
|
if (!parsedEmailContext || !parsedEmailContext.rawEmail) {
|
|
return undefined;
|
|
}
|
|
// return parsed email if already parsed
|
|
if (parsedEmailContext.parsedEmail) {
|
|
return parsedEmailContext.parsedEmail;
|
|
}
|
|
const raw_mail = parsedEmailContext.rawEmail;
|
|
// TODO: WASM parse email
|
|
// try {
|
|
// const { parse_message_wrapper } = await import('mail-parser-wasm-worker');
|
|
|
|
// const parsedEmail = parse_message_wrapper(raw_mail);
|
|
// parsedEmailContext.parsedEmail = {
|
|
// sender: parsedEmail.sender || "",
|
|
// subject: parsedEmail.subject || "",
|
|
// text: parsedEmail.text || "",
|
|
// headers: parsedEmail.headers?.map(
|
|
// (header) => ({ key: header.key, value: header.value })
|
|
// ) || [],
|
|
// html: parsedEmail.body_html || "",
|
|
// };
|
|
// return parsedEmailContext.parsedEmail;
|
|
// } catch (e) {
|
|
// console.error("Failed use mail-parser-wasm-worker to parse email", e);
|
|
// }
|
|
try {
|
|
const { default: PostalMime } = await import('postal-mime');
|
|
const parsedEmail = await PostalMime.parse(raw_mail);
|
|
parsedEmailContext.parsedEmail = {
|
|
sender: parsedEmail.from ? `${parsedEmail.from.name} <${parsedEmail.from.address}>` : "",
|
|
subject: parsedEmail.subject || "",
|
|
text: parsedEmail.text || "",
|
|
html: parsedEmail.html || "",
|
|
headers: parsedEmail.headers || [],
|
|
};
|
|
return parsedEmailContext.parsedEmail;
|
|
}
|
|
catch (e) {
|
|
console.error("Failed use PostalMime to parse email", e);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export const commonGetUserRole = async (
|
|
c: Context<HonoCustomType>, user_id: number
|
|
): Promise<UserRole | undefined | null> => {
|
|
const user_roles = getUserRoles(c);
|
|
const role_text = await c.env.DB.prepare(
|
|
`SELECT role_text FROM user_roles where user_id = ?`
|
|
).bind(user_id).first<string | undefined | null>("role_text");
|
|
return role_text ? user_roles.find((r) => r.role === role_text) : null;
|
|
}
|
|
|
|
export const getAddressPrefix = async (c: Context<HonoCustomType>): Promise<string | undefined> => {
|
|
const user = c.get("userPayload");
|
|
if (!user) {
|
|
return getStringValue(c.env.PREFIX);
|
|
}
|
|
const user_role = await commonGetUserRole(c, user.user_id);
|
|
if (typeof user_role?.prefix === "string") {
|
|
return user_role.prefix;
|
|
}
|
|
return getStringValue(c.env.PREFIX);
|
|
}
|
|
|
|
export const getAllowDomains = async (c: Context<HonoCustomType>): Promise<string[]> => {
|
|
const user = c.get("userPayload");
|
|
if (!user) {
|
|
return getDefaultDomains(c);
|
|
}
|
|
const user_role = await commonGetUserRole(c, user.user_id);
|
|
return user_role?.domains || getDefaultDomains(c);;
|
|
}
|
|
|
|
export async function sendWebhook(
|
|
settings: WebhookSettings, formatMap: WebhookMail
|
|
): Promise<{ success: boolean, message?: string }> {
|
|
// send webhook
|
|
let body = settings.body;
|
|
for (const key of Object.keys(formatMap)) {
|
|
body = body.replace(
|
|
new RegExp(`\\$\\{${key}\\}`, "g"),
|
|
JSON.stringify(
|
|
formatMap[key as keyof WebhookMail]
|
|
).replace(/^"(.*)"$/, '$1')
|
|
);
|
|
}
|
|
const response = await fetch(settings.url, {
|
|
method: settings.method,
|
|
headers: JSON.parse(settings.headers),
|
|
body: body
|
|
});
|
|
if (!response.ok) {
|
|
console.log("send webhook error", settings.url, settings.method, settings.headers, body);
|
|
console.log("send webhook error", response.status, response.statusText);
|
|
return { success: false, message: `send webhook error: ${response.status} ${response.statusText}` };
|
|
}
|
|
return { success: true }
|
|
}
|
|
|
|
export async function triggerWebhook(
|
|
c: Context<HonoCustomType>,
|
|
address: string,
|
|
parsedEmailContext: ParsedEmailContext,
|
|
message_id: string | null
|
|
): Promise<void> {
|
|
if (!c.env.KV || !getBooleanValue(c.env.ENABLE_WEBHOOK)) {
|
|
return
|
|
}
|
|
const webhookList: WebhookSettings[] = []
|
|
|
|
// admin mail webhook
|
|
const adminMailWebhookSettings = await c.env.KV.get<WebhookSettings>(CONSTANTS.WEBHOOK_KV_ADMIN_MAIL_SETTINGS_KEY, "json");
|
|
if (adminMailWebhookSettings?.enabled) {
|
|
webhookList.push(adminMailWebhookSettings)
|
|
}
|
|
|
|
// user mail webhook
|
|
const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
|
|
if (!adminSettings?.enableAllowList || adminSettings?.allowList.includes(address)) {
|
|
const settings = await c.env.KV.get<WebhookSettings>(
|
|
`${CONSTANTS.WEBHOOK_KV_USER_SETTINGS_KEY}:${address}`, "json"
|
|
);
|
|
if (settings?.enabled) {
|
|
webhookList.push(settings)
|
|
}
|
|
}
|
|
|
|
// no webhook
|
|
if (webhookList.length === 0) {
|
|
return
|
|
}
|
|
const mailId = await c.env.DB.prepare(
|
|
`SELECT id FROM raw_mails where address = ? and message_id = ?`
|
|
).bind(address, message_id).first<string>("id");
|
|
|
|
const parsedEmail = await commonParseMail(parsedEmailContext);
|
|
const webhookMail = {
|
|
id: mailId || "",
|
|
url: c.env.FRONTEND_URL ? `${c.env.FRONTEND_URL}?mail_id=${mailId}` : "",
|
|
from: parsedEmail?.sender || "",
|
|
to: address,
|
|
subject: parsedEmail?.subject || "",
|
|
raw: parsedEmailContext.rawEmail || "",
|
|
parsedText: parsedEmail?.text || "",
|
|
parsedHtml: parsedEmail?.html || ""
|
|
}
|
|
for (const settings of webhookList) {
|
|
const res = await sendWebhook(settings, webhookMail);
|
|
if (!res.success) {
|
|
console.error(res.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function triggerAnotherWorker(
|
|
c: Context<HonoCustomType>,
|
|
rpcEmailMessage: RPCEmailMessage,
|
|
parsedText: string | undefined | null
|
|
): Promise<void> {
|
|
if (!parsedText) {
|
|
return;
|
|
}
|
|
|
|
const anotherWorkerList: AnotherWorker[] = getAnotherWorkerList(c);
|
|
if (!getBooleanValue(c.env.ENABLE_ANOTHER_WORKER) || anotherWorkerList.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const parsedTextLowercase: string = parsedText.toLowerCase();
|
|
for (const worker of anotherWorkerList) {
|
|
|
|
const keywords = worker?.keywords ?? [];
|
|
const bindingName = worker?.binding ?? "";
|
|
const methodName = worker.method ?? "rpcEmail";
|
|
|
|
const serviceBinding = (c.env as any)[bindingName] ?? {};
|
|
const method = serviceBinding[methodName];
|
|
|
|
if (!method || typeof method !== "function") {
|
|
console.log(`method = ${methodName} not found or not function`);
|
|
continue;
|
|
}
|
|
|
|
if (!keywords.some(keyword => keyword && parsedTextLowercase.includes(keyword.toLowerCase()))) {
|
|
console.log(`worker.binding = ${bindingName} not match keywords, parsedText = ${parsedText}`);
|
|
continue;
|
|
}
|
|
try {
|
|
const bodyObj = { ...rpcEmailMessage } as any;
|
|
if (bodyObj.headers && typeof bodyObj.headers.forEach === "function") {
|
|
const headerObj: any = {}
|
|
bodyObj.headers.forEach((value: string, key: string) => {
|
|
headerObj[key] = value;
|
|
});
|
|
bodyObj.headers = headerObj
|
|
}
|
|
const requestBody = JSON.stringify(bodyObj);
|
|
console.log(`exec worker , binding = ${bindingName} , requestBody = ${requestBody}`);
|
|
await method(requestBody);
|
|
} catch (e1) {
|
|
console.error(`execute method = ${methodName} error`, e1);
|
|
}
|
|
}
|
|
}
|