fix: auto-reply not triggering when source_prefix is empty (#880)

* fix: auto-reply not triggering when source_prefix is empty (#459)

- Empty source_prefix now matches all senders (was short-circuiting as falsy)
- Support regex matching with /pattern/ syntax in source_prefix
- Backward compatible: plain strings still use startsWith
- Use E2E_TEST_MODE switch to skip cloudflare:email import in tests
- Track reply() calls in E2E mock for testability

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: update auto-reply UI labels for regex support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: update changelogs for auto-reply fix and regex feature

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: upgrade version to v1.5.0

- Update version number to 1.5.0 in all package.json files and constants.ts
- Split CHANGELOG: v1.4.0 entries finalized, new v1.5.0(main) section with auto-reply changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add error logging for invalid regex in auto-reply source_prefix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: address CodeRabbit review suggestions

- Use const object instead of let for mock state tracking
- Add log when auto-reply subject/message falls back to defaults

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add source_prefix regex syntax to auto-reply docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dream Hunter
2026-03-08 19:08:06 +08:00
committed by GitHub
parent 10873e7887
commit 5f3762ef58
13 changed files with 272 additions and 26 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "cloudflare_temp_email",
"version": "1.4.0",
"version": "1.5.0",
"private": true,
"type": "module",
"scripts": {

View File

@@ -44,20 +44,19 @@ const receiveMail = async (c: Context<HonoCustomType>) => {
if (!headers.has('Message-ID')) headers.set('Message-ID', `<e2e-${Date.now()}@test>`);
const rawBytes = new TextEncoder().encode(raw);
let rejected: string | undefined;
const state = { rejected: undefined as string | undefined, replyCalled: false };
const mockMessage: ForwardableEmailMessage = {
from, to, headers,
rawSize: rawBytes.byteLength,
raw: new ReadableStream({ start(ctrl) { ctrl.enqueue(rawBytes); ctrl.close(); } }),
setReject(reason: string) { rejected = reason; },
setReject(reason: string) { state.rejected = reason; },
forward: async () => ({ messageId: '' }),
reply: async () => ({ messageId: '' }),
reply: async () => { state.replyCalled = true; return { messageId: '' }; },
};
const { email: emailHandler } = await import('../email');
await emailHandler(mockMessage, c.env, { waitUntil: () => {}, passThroughOnException: () => {} });
return c.json({ success: !rejected, ...(rejected ? { rejected } : {}) });
return c.json({ success: !state.rejected, replyCalled: state.replyCalled, ...(state.rejected ? { rejected: state.rejected } : {}) });
};
export default { seedMail, receiveMail };

View File

@@ -1,5 +1,5 @@
export const CONSTANTS = {
VERSION: 'v' + '1.4.0',
VERSION: 'v' + '1.5.0',
// DB Version
DB_VERSION_KEY: 'db_version',

View File

@@ -1,6 +1,26 @@
import { createMimeMessage } from "mimetext";
import { getBooleanValue } from "../utils";
/**
* Check if the sender matches the source_prefix filter.
* - empty/undefined: match all senders
* - starts and ends with `/`: treat as regex (e.g. `/.*@example\.com$/`)
* - otherwise: legacy startsWith match
*/
function matchSender(from: string, sourcePrefix: string | undefined): boolean {
if (!sourcePrefix) return true;
if (sourcePrefix.startsWith("/") && sourcePrefix.endsWith("/") && sourcePrefix.length > 2) {
try {
const regex = new RegExp(sourcePrefix.slice(1, -1));
return regex.test(from);
} catch (error) {
console.error("Invalid regex in source_prefix:", sourcePrefix, error);
return false;
}
}
return from.startsWith(sourcePrefix);
}
export const auto_reply = async (message: ForwardableEmailMessage, env: Bindings): Promise<void> => {
const message_id = message.headers.get("Message-ID");
// auto reply email
@@ -9,7 +29,10 @@ export const auto_reply = async (message: ForwardableEmailMessage, env: Bindings
const results = await env.DB.prepare(
`SELECT * FROM auto_reply_mails where address = ? and enabled = 1`
).bind(message.to).first<Record<string, string>>();
if (results && results.source_prefix && message.from.startsWith(results.source_prefix)) {
if (results && matchSender(message.from, results.source_prefix)) {
if (!results.subject || !results.message) {
console.log("auto-reply using defaults:", !results.subject ? "subject" : "", !results.message ? "message" : "");
}
const msg = createMimeMessage();
msg.setHeader("In-Reply-To", message_id);
msg.setSender({
@@ -22,14 +45,18 @@ export const auto_reply = async (message: ForwardableEmailMessage, env: Bindings
contentType: 'text/plain',
data: results.message || "This is an auto-reply message, please reconact later."
});
const { EmailMessage } = await import('cloudflare:email');
const replyMessage = new EmailMessage(
message.to,
message.from,
msg.asRaw()
);
// @ts-ignore
await message.reply(replyMessage);
if (getBooleanValue(env.E2E_TEST_MODE)) {
await message.reply(msg.asRaw());
} else {
const { EmailMessage } = await import('cloudflare:email');
const replyMessage = new EmailMessage(
message.to,
message.from,
msg.asRaw()
);
// @ts-ignore
await message.reply(replyMessage);
}
}
} catch (error) {
console.log("reply email error", error);