mirror of
https://github.com/kennyparsons/cf-emailrouter.git
synced 2025-09-01 01:59:52 +00:00
refactored routes, organized, console logging
This commit is contained in:
parent
679b565acf
commit
acb20e82a1
4 changed files with 243 additions and 214 deletions
14
src/auth.js
Normal file
14
src/auth.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
// src/auth.js
|
||||
export async function isAuthorized(request, env) {
|
||||
const auth = request.headers.get("Authorization") || "";
|
||||
//console.log("Authorization header received:", auth);
|
||||
|
||||
const expected = await env.API_AUTH.get(env.WORKER_NAME);
|
||||
if (!expected) {
|
||||
console.warn("No API key set for worker " + env.WORKER_NAME);
|
||||
return false;
|
||||
}
|
||||
|
||||
const isValid = auth === `Bearer ${expected}`;
|
||||
return isValid;
|
||||
}
|
136
src/email/main.js
Normal file
136
src/email/main.js
Normal file
|
@ -0,0 +1,136 @@
|
|||
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;
|
||||
if (config.allow.domains && config.allow.domains.includes(senderDomain)) {
|
||||
allowed = true;
|
||||
log('debug', `Sender domain ${senderDomain} is allowed.`);
|
||||
}
|
||||
if (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}`);
|
||||
// if (config.logging && config.logging.log_sender_domain) {
|
||||
// log('warn', `Sender domain ${senderDomain} 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");
|
||||
}
|
||||
}
|
235
src/index.js
235
src/index.js
|
@ -1,224 +1,31 @@
|
|||
import { applyDefaults } from "./schema.js";
|
||||
import * as API from "./routes/api.js";
|
||||
import { handleEmail } from "./email/main.js";
|
||||
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
const url = new URL(request.url);
|
||||
const pathname = url.pathname;
|
||||
const path = url.pathname;
|
||||
|
||||
// Handle API routes under /api/email
|
||||
if (pathname.startsWith("/api/email")) {
|
||||
return handleApiRequest(request, env);
|
||||
// Visually define our expected routes using a switch on conditions
|
||||
switch (true) {
|
||||
// API route for listing all email configurations
|
||||
case path === "/api/email/list":
|
||||
return API.handleEmailList(request, env, ctx);
|
||||
|
||||
// API route for handling a specific email configuration
|
||||
case /^\/api\/email\/([^\/]+)\/?$/.test(path): {
|
||||
const match = path.match(/^\/api\/email\/([^\/]+)\/?$/);
|
||||
const emailParam = decodeURIComponent(match[1]);
|
||||
return API.handleEmailConfig(request, env, ctx, emailParam);
|
||||
}
|
||||
|
||||
// Default: no matching route, return 404
|
||||
default:
|
||||
return new Response("Not found", { status: 404 });
|
||||
}
|
||||
|
||||
// Fallback: serve UI or return 404 if no matching route
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
|
||||
async email(message, env, ctx) {
|
||||
await handleEmail(message, env, ctx);
|
||||
return handleEmail(message, env, ctx);
|
||||
}
|
||||
};
|
||||
|
||||
async function handleEmail(message, env, ctx) {
|
||||
// Retrieve configuration for the recipient email address from KV.
|
||||
const configStr = await env.EMAIL_KV.get(message.to);
|
||||
if (!configStr) {
|
||||
console.warn(`No configuration 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);
|
||||
} catch (e) {
|
||||
console.error("Invalid JSON configuration:", e);
|
||||
message.setReject("Invalid routing config");
|
||||
return;
|
||||
}
|
||||
|
||||
// Defensive check for new field 'site_origin'
|
||||
const siteOrigin = config.site_origin || "";
|
||||
if (siteOrigin) {
|
||||
// Ensure the 'allow' object exists and has a 'domains' array
|
||||
if (!config.allow) {
|
||||
config.allow = { domains: [], emails: [] };
|
||||
} else if (!config.allow.domains) {
|
||||
config.allow.domains = [];
|
||||
}
|
||||
// Add site_origin to allowed domains if not already present
|
||||
if (!config.allow.domains.includes(siteOrigin)) {
|
||||
console.log(`Adding site_origin '${siteOrigin}' to allowed domains for ${message.to}`);
|
||||
config.allow.domains.push(siteOrigin);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the configuration is enabled.
|
||||
if (!config.enabled) {
|
||||
console.log(`Configuration for ${message.to} is disabled.`);
|
||||
message.setReject("Service disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract sender details.
|
||||
const sender = message.from;
|
||||
const senderDomain = sender.split("@")[1].toLowerCase();
|
||||
|
||||
// Allow list: if defined, the sender must match an allowed domain or email.
|
||||
if (config.allow) {
|
||||
let allowed = false;
|
||||
if (config.allow.domains && config.allow.domains.includes(senderDomain)) {
|
||||
allowed = true;
|
||||
}
|
||||
if (config.allow.emails && config.allow.emails.includes(sender)) {
|
||||
allowed = true;
|
||||
}
|
||||
if (!allowed) {
|
||||
if (config.logging && config.logging.log_sender_domain) {
|
||||
console.warn(`Sender domain ${senderDomain} 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))
|
||||
) {
|
||||
console.warn(`Sender ${sender} is explicitly denied for ${message.to}`);
|
||||
message.setReject("Sender denied");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Filtering: iterate through any filtering rules (e.g., reject if subject matches a pattern).
|
||||
if (config.filtering && Array.isArray(config.filtering)) {
|
||||
for (const rule of config.filtering) {
|
||||
const subject = message.subject || "";
|
||||
if (subject.toLowerCase().includes(rule.pattern.toLowerCase())) {
|
||||
if (rule.action === "reject") {
|
||||
console.warn(`Email subject matches filter rule "${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];
|
||||
|
||||
for (const addr of forwardingAddresses) {
|
||||
await message.forward(addr);
|
||||
}
|
||||
} else {
|
||||
console.warn(`No forwarding address configured for ${message.to}`);
|
||||
message.setReject("No forwarding address configured");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApiRequest(request, env) {
|
||||
// Ensure API requests are authorized via our central API_AUTH KV store
|
||||
if (!(await isAuthorized(request, env))) {
|
||||
console.log("Authorization failed");
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const parts = url.pathname.split("/").filter(Boolean); // e.g., ["api", "email", "list"] or ["api", "email", "<email>"]
|
||||
|
||||
// Validate that the URL follows our expected pattern: /api/email/...
|
||||
if (parts.length < 2 || parts[0] !== "api" || parts[1] !== "email") {
|
||||
return new Response("Bad request", { status: 400 });
|
||||
}
|
||||
|
||||
// Route: /api/email/list
|
||||
if (parts[2] === "list") {
|
||||
if (request.method === "GET") {
|
||||
const list = await env.EMAIL_KV.list();
|
||||
const emails = await Promise.all(
|
||||
list.keys.map(async (entry) => {
|
||||
const val = await env.EMAIL_KV.get(entry.name);
|
||||
let config;
|
||||
try {
|
||||
config = JSON.parse(val);
|
||||
config = applyDefaults(config);
|
||||
} catch (e) {
|
||||
// In case the stored value isn't valid JSON, return it as-is.
|
||||
config = val;
|
||||
}
|
||||
return { email: entry.name, config };
|
||||
})
|
||||
);
|
||||
return new Response(JSON.stringify(emails), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
} else {
|
||||
return new Response("Method Not Allowed", { status: 405 });
|
||||
}
|
||||
}
|
||||
|
||||
// Route: /api/email/:email
|
||||
const emailKey = decodeURIComponent(parts[2]);
|
||||
switch (request.method) {
|
||||
case "GET": {
|
||||
const val = await env.EMAIL_KV.get(emailKey);
|
||||
if (val) {
|
||||
let config;
|
||||
try {
|
||||
config = JSON.parse(val);
|
||||
config = applyDefaults(config);
|
||||
} catch (e) {
|
||||
config = val;
|
||||
}
|
||||
return new Response(JSON.stringify(config), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
} else {
|
||||
return new Response("Not found", { status: 404 });
|
||||
}
|
||||
}
|
||||
case "PUT": {
|
||||
try {
|
||||
const body = await request.json();
|
||||
await env.EMAIL_KV.put(emailKey, JSON.stringify(body));
|
||||
return new Response("Saved", { status: 200 });
|
||||
} catch (error) {
|
||||
return new Response("Invalid JSON", { status: 400 });
|
||||
}
|
||||
}
|
||||
case "DELETE": {
|
||||
await env.EMAIL_KV.delete(emailKey);
|
||||
return new Response("Deleted", { status: 200 });
|
||||
}
|
||||
default: {
|
||||
return new Response("Bad request", { status: 400 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function isAuthorized(request, env) {
|
||||
const auth = request.headers.get("Authorization") || "";
|
||||
console.log("Authorization header received:", auth);
|
||||
|
||||
const expected = await env.API_AUTH.get(env.WORKER_NAME);
|
||||
// console.log("Expected API key from KV for worker", env.WORKER_NAME, ":", expected);
|
||||
|
||||
if (!expected) {
|
||||
console.warn("No API key set for worker " + env.WORKER_NAME);
|
||||
return false;
|
||||
}
|
||||
|
||||
const isValid = auth === `Bearer ${expected}`;
|
||||
// console.log("Authorization", isValid);
|
||||
return isValid;
|
||||
}
|
||||
};
|
72
src/routes/api.js
Normal file
72
src/routes/api.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
import { applyDefaults } from "../schema.js";
|
||||
import { isAuthorized } from "../auth.js";
|
||||
|
||||
export async function handleEmailList(request, env, ctx) {
|
||||
if (!(await isAuthorized(request, env))) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
switch (request.method) {
|
||||
case "GET": {
|
||||
const list = await env.EMAIL_KV.list();
|
||||
const emails = await Promise.all(
|
||||
list.keys.map(async (entry) => {
|
||||
const val = await env.EMAIL_KV.get(entry.name);
|
||||
let config;
|
||||
try {
|
||||
config = JSON.parse(val);
|
||||
config = applyDefaults(config);
|
||||
} catch (e) {
|
||||
config = val;
|
||||
}
|
||||
return { email: entry.name, config };
|
||||
})
|
||||
);
|
||||
return new Response(JSON.stringify(emails), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
default:
|
||||
return new Response("Method Not Allowed", { status: 405 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleEmailConfig(request, env, ctx, emailKey) {
|
||||
if (!(await isAuthorized(request, env))) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
switch (request.method) {
|
||||
case "GET": {
|
||||
const val = await env.EMAIL_KV.get(emailKey);
|
||||
if (val) {
|
||||
let config;
|
||||
try {
|
||||
config = JSON.parse(val);
|
||||
config = applyDefaults(config);
|
||||
} catch (e) {
|
||||
config = val;
|
||||
}
|
||||
return new Response(JSON.stringify(config), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
} else {
|
||||
return new Response("Not found", { status: 404 });
|
||||
}
|
||||
}
|
||||
case "PUT": {
|
||||
try {
|
||||
const body = await request.json();
|
||||
await env.EMAIL_KV.put(emailKey, JSON.stringify(body));
|
||||
return new Response("Saved", { status: 200 });
|
||||
} catch (error) {
|
||||
return new Response("Invalid JSON", { status: 400 });
|
||||
}
|
||||
}
|
||||
case "DELETE": {
|
||||
await env.EMAIL_KV.delete(emailKey);
|
||||
return new Response("Deleted", { status: 200 });
|
||||
}
|
||||
default:
|
||||
return new Response("Method Not Allowed", { status: 405 });
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue