Skip to content
File

Blob: src/worker/services/inbox/lifecycle.ts

typescript138 lines
1import { and, eq } from "drizzle-orm";
2import { nanoid } from "nanoid";
3import type { SessionRecord, TempMailboxTtlHours } from "@/shared/contracts";
4import { createDb, type Database } from "@/worker/db";
5import { domains, inboxes } from "@/worker/db/schema";
6import { createLogger } from "@/worker/logger";
7import { PublicError } from "@/worker/security";
8import type { InboxRecord } from "@/worker/types";
9import { purgeInboxes } from "./cleanup";
10import { createSessionToken, refreshSessionToken } from "./session-store";
11import { computeInboxExpiry, createLocalPart, hoursToMs, ttlHoursFromInbox } from "./shared";
12 
13const logger = createLogger("inbox-service");
14 
15export 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 
87export 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 
126export 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}