File
Blob: src/worker/services/inbox/lifecycle.ts
| 1 | import { and, eq } from "drizzle-orm"; |
| 2 | import { nanoid } from "nanoid"; |
| 3 | import type { SessionRecord, TempMailboxTtlHours } from "@/shared/contracts"; |
| 4 | import { createDb, type Database } from "@/worker/db"; |
| 5 | import { domains, inboxes } from "@/worker/db/schema"; |
| 6 | import { createLogger } from "@/worker/logger"; |
| 7 | import { PublicError } from "@/worker/security"; |
| 8 | import type { InboxRecord } from "@/worker/types"; |
| 9 | import { purgeInboxes } from "./cleanup"; |
| 10 | import { createSessionToken, refreshSessionToken } from "./session-store"; |
| 11 | import { computeInboxExpiry, createLocalPart, hoursToMs, ttlHoursFromInbox } from "./shared"; |
| 12 | |
| 13 | const logger = createLogger("inbox-service"); |
| 14 | |
| 15 | export async function createTemporaryInbox( |
| 16 | env: Env, |
| 17 | requestedDomain: string, |
| 18 | ttlHours: TempMailboxTtlHours, |
| 19 | db?: Database, |
| 20 | ) { |
| 21 | const database = db ?? createDb(env.DB); |
| 22 | const domainRecord = await database.query.domains.findFirst({ |
| 23 | where: and(eq(domains.domain, requestedDomain), eq(domains.isActive, true)), |
| 24 | }); |
| 25 | |
| 26 | if (!domainRecord) { |
| 27 | throw new PublicError("Requested domain is not available"); |
| 28 | } |
| 29 | |
| 30 | let localPart = ""; |
| 31 | let address = ""; |
| 32 | let existing: InboxRecord | undefined; |
| 33 | |
| 34 | for (let attempt = 0; attempt < 8; attempt += 1) { |
| 35 | localPart = createLocalPart(); |
| 36 | address = `${localPart}@${requestedDomain}`; |
| 37 | existing = await database.query.inboxes.findFirst({ |
| 38 | where: eq(inboxes.fullAddress, address), |
| 39 | }); |
| 40 | |
| 41 | if (!existing) { |
| 42 | break; |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | if (existing) { |
| 47 | throw new PublicError("Could not create an inbox right now"); |
| 48 | } |
| 49 | |
| 50 | const createdAt = new Date(); |
| 51 | const expiresAt = computeInboxExpiry(createdAt, ttlHours); |
| 52 | const inboxId = nanoid(); |
| 53 | |
| 54 | await database.insert(inboxes).values({ |
| 55 | id: inboxId, |
| 56 | localPart, |
| 57 | domain: requestedDomain, |
| 58 | fullAddress: address, |
| 59 | isPermanent: false, |
| 60 | createdAt, |
| 61 | expiresAt, |
| 62 | }); |
| 63 | |
| 64 | const token = await createSessionToken( |
| 65 | env, |
| 66 | { |
| 67 | type: "user", |
| 68 | address, |
| 69 | }, |
| 70 | hoursToMs(ttlHours), |
| 71 | ); |
| 72 | |
| 73 | logger.info("inbox_created", "Created temporary inbox", { |
| 74 | address, |
| 75 | domain: requestedDomain, |
| 76 | ttlHours, |
| 77 | }); |
| 78 | |
| 79 | return { |
| 80 | address, |
| 81 | token, |
| 82 | ttlHours, |
| 83 | expiresAt, |
| 84 | }; |
| 85 | } |
| 86 | |
| 87 | export async function extendTemporaryInbox( |
| 88 | env: Env, |
| 89 | inbox: InboxRecord, |
| 90 | token: string, |
| 91 | session: SessionRecord, |
| 92 | ttlHours: TempMailboxTtlHours, |
| 93 | db?: Database, |
| 94 | ) { |
| 95 | if (inbox.isPermanent) { |
| 96 | throw new PublicError("Permanent inboxes cannot be extended"); |
| 97 | } |
| 98 | |
| 99 | const expiresAt = computeInboxExpiry(inbox.createdAt, ttlHours); |
| 100 | const currentExpiresAt = inbox.expiresAt; |
| 101 | |
| 102 | if (!currentExpiresAt) { |
| 103 | throw new PublicError("Inbox does not have an expiry to extend"); |
| 104 | } |
| 105 | |
| 106 | if (expiresAt.getTime() <= currentExpiresAt.getTime()) { |
| 107 | throw new PublicError("Inbox already extends beyond the requested duration"); |
| 108 | } |
| 109 | |
| 110 | const database = db ?? createDb(env.DB); |
| 111 | await database.update(inboxes).set({ expiresAt }).where(eq(inboxes.id, inbox.id)); |
| 112 | await refreshSessionToken(env, token, session, expiresAt); |
| 113 | |
| 114 | logger.info("inbox_extended", "Extended temporary inbox lifetime", { |
| 115 | address: inbox.fullAddress, |
| 116 | ttlHours, |
| 117 | previousTtlHours: ttlHoursFromInbox(inbox), |
| 118 | }); |
| 119 | |
| 120 | return { |
| 121 | expiresAt, |
| 122 | ttlHours, |
| 123 | }; |
| 124 | } |
| 125 | |
| 126 | export async function deleteInbox(env: Env, inbox: InboxRecord, db?: Database) { |
| 127 | if (inbox.isPermanent) { |
| 128 | throw new PublicError("Permanent inboxes cannot be deleted"); |
| 129 | } |
| 130 | |
| 131 | const database = db ?? createDb(env.DB); |
| 132 | await purgeInboxes(env, [inbox], database); |
| 133 | |
| 134 | logger.info("inbox_deleted", "Deleted inbox and associated storage", { |
| 135 | address: inbox.fullAddress, |
| 136 | }); |
| 137 | } |