diff --git a/src/index.ts b/src/index.ts index 6f33ab0..cf0c7b6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,11 @@ import { hubRouter } from "./routes/hub"; import { handleCloudflareEmail } from "./lib/cloudflare-email"; import { Env } from "./types"; import { logger } from "./lib/logger"; +import { + listAllFeeds, + purgeExpiredFeeds, + removeFeedsFromListBulk, +} from "./routes/admin/helpers"; import { FORWARD_EMAIL_IPS_CACHE_TTL_MS } from "./config/constants"; type AppEnv = { Bindings: Env }; @@ -176,16 +181,18 @@ export default { await handleCloudflareEmail(message, env, ctx); }, async scheduled(_event: ScheduledEvent, env: Env, _ctx: ExecutionContext) { - let cursor: string | undefined; - let deleted = 0; - do { - const result = await env.EMAIL_STORAGE.list({ cursor }); - await Promise.all( - result.keys.map(({ name }) => env.EMAIL_STORAGE.delete(name)), - ); - deleted += result.keys.length; - cursor = result.list_complete ? undefined : result.cursor; - } while (cursor); - logger.info("Demo KV reset complete", { deleted }); + const feeds = await listAllFeeds(env.EMAIL_STORAGE); + const now = Date.now(); + const expiredIds = feeds + .filter((f) => f.expires_at !== undefined && f.expires_at <= now) + .map((f) => f.id); + + for (const feedId of expiredIds) { + await purgeExpiredFeeds(env.EMAIL_STORAGE, feedId, env.ATTACHMENT_BUCKET); + } + if (expiredIds.length > 0) { + await removeFeedsFromListBulk(env.EMAIL_STORAGE, expiredIds); + logger.info("Feed TTL cleanup", { deleted: expiredIds.length }); + } }, }; diff --git a/src/lib/email-processor.ts b/src/lib/email-processor.ts index 43a28d9..e3b6e87 100644 --- a/src/lib/email-processor.ts +++ b/src/lib/email-processor.ts @@ -112,6 +112,16 @@ export async function validateEmail( response: new Response("Feed does not exist", { status: 404 }), }; } + if ( + feedConfig.expires_at !== undefined && + feedConfig.expires_at <= Date.now() + ) { + logger.warn("Rejected email: feed expired", { feedId }); + return { + ok: false, + response: new Response("Feed has expired", { status: 410 }), + }; + } const allowedSenders = (feedConfig.allowed_senders || []) .map(normalizeEmail) diff --git a/src/routes/admin.tsx b/src/routes/admin.tsx index cb55f9a..abcdd9c 100644 --- a/src/routes/admin.tsx +++ b/src/routes/admin.tsx @@ -279,6 +279,35 @@ const CopyFieldInline = ({ value }: CopyFieldInlineProps) => ( ); +function formatExpiry(expiresAt: number): { label: string; expired: boolean } { + const remaining = expiresAt - Date.now(); + if (remaining <= 0) { + const h = Math.floor(-remaining / 3_600_000); + return { + label: h > 0 ? `Expired ${h}h ago` : "Just expired", + expired: true, + }; + } + const h = Math.floor(remaining / 3_600_000); + if (h >= 48) { + return { label: `Expires in ${Math.floor(h / 24)}d`, expired: false }; + } + const m = Math.floor((remaining % 3_600_000) / 60_000); + return { + label: h > 0 ? `Expires in ${h}h ${m}m` : `Expires in ${m}m`, + expired: false, + }; +} + +const ExpiryBadge = ({ expiresAt }: { expiresAt: number }) => { + const { label, expired } = formatExpiry(expiresAt); + return ( + + {label} + + ); +}; + // Admin dashboard route app.get("/", async (c) => { // Type assertion for environment variables @@ -396,6 +425,29 @@ app.get("/", async (c) => { +