cf-emailrouter/src/email/main.js
2025-04-23 15:02:50 -05:00

155 lines
No EOL
5.8 KiB
JavaScript

import { applyDefaults } from "../schema.js";
// Global logging level: set to 4 (debug) to log everything.
// Logging level definitions:
// 0: No logging, 1: error, 2: info, 3: warn, 4: debug.
const currentLoggingLevel = 4;
const loggingLevels = {
error: 1,
info: 2,
warn: 3,
debug: 4,
};
function log(level, message, ...optionalParams) {
if (loggingLevels[level] <= currentLoggingLevel && currentLoggingLevel > 0) {
switch (level) {
case 'debug':
console.debug(message, ...optionalParams);
break;
case 'info':
console.info(message, ...optionalParams);
break;
case 'warn':
console.warn(message, ...optionalParams);
break;
case 'error':
console.error(message, ...optionalParams);
break;
default:
console.log(message, ...optionalParams);
}
}
}
export async function handleEmail(message, env, ctx) {
// Log initial message details.
log('debug', `Received email: from=${message.from}, to=${message.to}, subject=${message.subject}`);
// Retrieve configuration for the recipient email address from KV.
log('debug', `Fetching configuration for recipient: ${message.to}`);
const configStr = await env.EMAIL_KV.get(message.to);
if (!configStr) {
log('warn', `No configuration found for recipient ${message.to}`);
message.setReject("No route defined");
return;
}
let config;
try {
config = JSON.parse(configStr);
// Apply default values for any missing configuration fields
config = applyDefaults(config);
log('debug', `Configuration for ${message.to} after applying defaults:`, config);
} catch (e) {
log('error', "Invalid JSON configuration:", e);
message.setReject("Invalid routing config");
return;
}
// Check if the configuration is enabled.
if (!config.enabled) {
log('info', `Configuration for ${message.to} is disabled.`);
message.setReject("Service disabled");
return;
}
// Extract sender details.
const sender = message.from;
const senderDomain = sender.split("@")[1].toLowerCase();
log('debug', `Parsed sender details: sender=${sender}, domain=${senderDomain}`);
// Allow list: if defined, the sender must match an allowed domain or email.
if (config.allow) {
let allowed = false;
// Check allowed domains.
if (config.allow.domains) {
for (const allowedDomain of config.allow.domains) {
// If the allowed domain contains a wildcard.
if (allowedDomain.includes('*')) {
// Convert wildcard to regex: escape non-wildcard parts then replace '*' with '.*'
const regexStr = '^' + allowedDomain.split('*')
.map(part => part.replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&'))
.join('.*') + '$';
const regex = new RegExp(regexStr, 'i');
if (regex.test(senderDomain)) {
allowed = true;
log('debug', `Sender domain ${senderDomain} is allowed by wildcard ${allowedDomain}.`);
break;
}
} else if (allowedDomain.toLowerCase() === senderDomain) {
allowed = true;
log('debug', `Sender domain ${senderDomain} is allowed.`);
break;
}
}
}
// Check allowed emails if not already allowed.
if (!allowed && config.allow.emails && config.allow.emails.includes(sender)) {
allowed = true;
log('debug', `Sender email ${sender} is allowed.`);
}
if (!allowed) {
log('warn', `Sender ${sender} is not allowed for ${message.to}`);
message.setReject("Sender not allowed");
return;
}
}
// Deny list: if defined, immediately reject if the sender is explicitly denied.
if (config.deny) {
if (
(config.deny.domains && config.deny.domains.includes(senderDomain)) ||
(config.deny.emails && config.deny.emails.includes(sender))
) {
log('warn', `Sender ${sender} is explicitly denied for ${message.to}`);
message.setReject("Sender denied");
return;
}
}
// Filtering: iterate through any filtering rules.
if (config.filtering && Array.isArray(config.filtering)) {
for (const rule of config.filtering) {
const subject = message.subject || "";
if (subject.toLowerCase().includes(rule.pattern.toLowerCase())) {
log('info', `Email subject matches filter rule "${rule.pattern}"`);
if (rule.action === "reject") {
log('warn', `Filter action 'reject' triggered by pattern "${rule.pattern}". Rejecting email.`);
message.setReject("Filtered email");
return;
}
// Additional filtering actions can be added here as needed.
}
}
}
// Forward the email to the destination(s) defined in the configuration.
if (config.forward_to) {
const forwardingAddresses = Array.isArray(config.forward_to)
? config.forward_to
: [config.forward_to];
log('debug', `Forwarding email for ${message.to} to: ${forwardingAddresses.join(", ")}`);
for (const addr of forwardingAddresses) {
await message.forward(addr);
log('debug', `Email forwarded to ${addr}`);
}
} else {
log('warn', `No forwarding address configured for ${message.to}`);
message.setReject("No forwarding address configured");
}
}