refactored routes, organized, console logging

This commit is contained in:
Kenny Parsons 2025-04-08 10:32:49 -05:00
parent 679b565acf
commit acb20e82a1
4 changed files with 243 additions and 214 deletions

14
src/auth.js Normal file
View 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
View 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");
}
}

View file

@ -1,224 +1,31 @@
import { applyDefaults } from "./schema.js"; import * as API from "./routes/api.js";
import { handleEmail } from "./email/main.js";
export default { export default {
async fetch(request, env, ctx) { async fetch(request, env, ctx) {
const url = new URL(request.url); const url = new URL(request.url);
const pathname = url.pathname; const path = url.pathname;
// Handle API routes under /api/email // Visually define our expected routes using a switch on conditions
if (pathname.startsWith("/api/email")) { switch (true) {
return handleApiRequest(request, env); // 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) { 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
View 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 });
}
}