@vllnt/convex-email 0.1.0-canary.63aca6b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -0
  3. package/dist/client/index.d.ts +174 -0
  4. package/dist/client/index.d.ts.map +1 -0
  5. package/dist/client/index.js +151 -0
  6. package/dist/client/index.js.map +1 -0
  7. package/dist/client/types.d.ts +84 -0
  8. package/dist/client/types.d.ts.map +1 -0
  9. package/dist/client/types.js +3 -0
  10. package/dist/client/types.js.map +1 -0
  11. package/dist/component/_generated/api.d.ts +40 -0
  12. package/dist/component/_generated/api.d.ts.map +1 -0
  13. package/dist/component/_generated/api.js +31 -0
  14. package/dist/component/_generated/api.js.map +1 -0
  15. package/dist/component/_generated/component.d.ts +110 -0
  16. package/dist/component/_generated/component.d.ts.map +1 -0
  17. package/dist/component/_generated/component.js +11 -0
  18. package/dist/component/_generated/component.js.map +1 -0
  19. package/dist/component/_generated/dataModel.d.ts +46 -0
  20. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  21. package/dist/component/_generated/dataModel.js +11 -0
  22. package/dist/component/_generated/dataModel.js.map +1 -0
  23. package/dist/component/_generated/server.d.ts +121 -0
  24. package/dist/component/_generated/server.d.ts.map +1 -0
  25. package/dist/component/_generated/server.js +78 -0
  26. package/dist/component/_generated/server.js.map +1 -0
  27. package/dist/component/convex.config.d.ts +3 -0
  28. package/dist/component/convex.config.d.ts.map +1 -0
  29. package/dist/component/convex.config.js +7 -0
  30. package/dist/component/convex.config.js.map +1 -0
  31. package/dist/component/crons.d.ts +16 -0
  32. package/dist/component/crons.d.ts.map +1 -0
  33. package/dist/component/crons.js +19 -0
  34. package/dist/component/crons.js.map +1 -0
  35. package/dist/component/mutations.d.ts +95 -0
  36. package/dist/component/mutations.d.ts.map +1 -0
  37. package/dist/component/mutations.js +243 -0
  38. package/dist/component/mutations.js.map +1 -0
  39. package/dist/component/queries.d.ts +59 -0
  40. package/dist/component/queries.d.ts.map +1 -0
  41. package/dist/component/queries.js +61 -0
  42. package/dist/component/queries.js.map +1 -0
  43. package/dist/component/schema.d.ts +54 -0
  44. package/dist/component/schema.d.ts.map +1 -0
  45. package/dist/component/schema.js +40 -0
  46. package/dist/component/schema.js.map +1 -0
  47. package/dist/component/validators.d.ts +54 -0
  48. package/dist/component/validators.d.ts.map +1 -0
  49. package/dist/component/validators.js +40 -0
  50. package/dist/component/validators.js.map +1 -0
  51. package/dist/jmap/index.d.ts +22 -0
  52. package/dist/jmap/index.d.ts.map +1 -0
  53. package/dist/jmap/index.js +21 -0
  54. package/dist/jmap/index.js.map +1 -0
  55. package/dist/jmap/send.d.ts +125 -0
  56. package/dist/jmap/send.d.ts.map +1 -0
  57. package/dist/jmap/send.js +418 -0
  58. package/dist/jmap/send.js.map +1 -0
  59. package/dist/jmap/types.d.ts +107 -0
  60. package/dist/jmap/types.d.ts.map +1 -0
  61. package/dist/jmap/types.js +16 -0
  62. package/dist/jmap/types.js.map +1 -0
  63. package/dist/shared.d.ts +32 -0
  64. package/dist/shared.d.ts.map +1 -0
  65. package/dist/shared.js +33 -0
  66. package/dist/shared.js.map +1 -0
  67. package/dist/smtp/index.d.ts +22 -0
  68. package/dist/smtp/index.d.ts.map +1 -0
  69. package/dist/smtp/index.js +21 -0
  70. package/dist/smtp/index.js.map +1 -0
  71. package/dist/smtp/send.d.ts +51 -0
  72. package/dist/smtp/send.d.ts.map +1 -0
  73. package/dist/smtp/send.js +124 -0
  74. package/dist/smtp/send.js.map +1 -0
  75. package/dist/smtp/transport.d.ts +43 -0
  76. package/dist/smtp/transport.d.ts.map +1 -0
  77. package/dist/smtp/transport.js +55 -0
  78. package/dist/smtp/transport.js.map +1 -0
  79. package/dist/smtp/types.d.ts +122 -0
  80. package/dist/smtp/types.d.ts.map +1 -0
  81. package/dist/smtp/types.js +9 -0
  82. package/dist/smtp/types.js.map +1 -0
  83. package/package.json +118 -0
  84. package/src/client/index.ts +312 -0
  85. package/src/client/types.ts +90 -0
  86. package/src/component/_generated/api.ts +56 -0
  87. package/src/component/_generated/component.ts +134 -0
  88. package/src/component/_generated/dataModel.ts +60 -0
  89. package/src/component/_generated/server.ts +156 -0
  90. package/src/component/convex.config.ts +9 -0
  91. package/src/component/crons.ts +23 -0
  92. package/src/component/mutations.ts +262 -0
  93. package/src/component/queries.ts +70 -0
  94. package/src/component/schema.ts +40 -0
  95. package/src/component/validators.ts +47 -0
  96. package/src/jmap/index.ts +39 -0
  97. package/src/jmap/send.test.ts +565 -0
  98. package/src/jmap/send.ts +502 -0
  99. package/src/jmap/types.ts +117 -0
  100. package/src/shared.ts +41 -0
  101. package/src/smtp/index.ts +30 -0
  102. package/src/smtp/send.test.ts +240 -0
  103. package/src/smtp/send.ts +154 -0
  104. package/src/smtp/transport.ts +58 -0
  105. package/src/smtp/types.ts +124 -0
  106. package/src/test.ts +12 -0
@@ -0,0 +1,243 @@
1
+ import { ConvexError, v } from "convex/values";
2
+ import { api } from "./_generated/api";
3
+ import { mutation } from "./_generated/server";
4
+ import { jsonValue } from "./validators";
5
+ /**
6
+ * Enqueue an outbound message and return its id immediately — the durable queue
7
+ * entry. The message is inserted in `queued` state with `attempts: 0`,
8
+ * `createdAt`/`updatedAt` stamped from the server clock (`Date.now()` inside the
9
+ * handler — never caller-supplied). The host owns the meaning of `to`/`from`, the
10
+ * opaque `payload`, and the `transport` adapter name — the component records the
11
+ * intent and never calls any provider. A transport sender later claims the
12
+ * message ({@link markSending}) and reports the outcome ({@link markSent} /
13
+ * {@link markFailed}).
14
+ *
15
+ * `messageId` is host-supplied (the host names its own message). The id must be
16
+ * unique — re-using an existing id throws
17
+ * `ConvexError({ code: "DUPLICATE_MESSAGE" })`. When `idempotencyKey` is supplied
18
+ * and a message already carries it, the existing message's id is returned and no
19
+ * new row is inserted — a retried enqueue (double-submit, at-least-once delivery)
20
+ * can never produce a duplicate send.
21
+ */
22
+ export const enqueue = mutation({
23
+ args: {
24
+ messageId: v.string(),
25
+ to: v.string(),
26
+ from: v.string(),
27
+ transport: v.string(),
28
+ payload: v.optional(jsonValue),
29
+ subjectRef: v.optional(v.string()),
30
+ idempotencyKey: v.optional(v.string()),
31
+ maxAttempts: v.number(),
32
+ },
33
+ returns: v.object({ messageId: v.string(), deduplicated: v.boolean() }),
34
+ handler: async (ctx, args) => {
35
+ if (args.idempotencyKey !== undefined) {
36
+ const dupe = await ctx.db
37
+ .query("messages")
38
+ .withIndex("by_idempotency_key", (q) => q.eq("idempotencyKey", args.idempotencyKey))
39
+ .unique();
40
+ if (dupe !== null) {
41
+ return { messageId: dupe.messageId, deduplicated: true };
42
+ }
43
+ }
44
+ const existing = await ctx.db
45
+ .query("messages")
46
+ .withIndex("by_message_id", (q) => q.eq("messageId", args.messageId))
47
+ .unique();
48
+ if (existing !== null) {
49
+ throw new ConvexError({
50
+ code: "DUPLICATE_MESSAGE",
51
+ message: `message "${args.messageId}" already exists`,
52
+ });
53
+ }
54
+ const now = Date.now();
55
+ await ctx.db.insert("messages", {
56
+ messageId: args.messageId,
57
+ to: args.to,
58
+ from: args.from,
59
+ transport: args.transport,
60
+ status: "queued",
61
+ payload: args.payload,
62
+ subjectRef: args.subjectRef,
63
+ idempotencyKey: args.idempotencyKey,
64
+ attempts: 0,
65
+ maxAttempts: args.maxAttempts,
66
+ createdAt: now,
67
+ updatedAt: now,
68
+ });
69
+ return { messageId: args.messageId, deduplicated: false };
70
+ },
71
+ });
72
+ /**
73
+ * Claim a `queued` message for a send attempt: move it to `sending` and increment
74
+ * `attempts`. A transport sender calls this before dispatching, so concurrent
75
+ * flushers don't double-send the same row. Re-claiming an already-`sending`
76
+ * message is rejected (another sender owns it). A terminal message is rejected.
77
+ *
78
+ * @throws `ConvexError({ code: "NOT_FOUND" })` when no message has `messageId`.
79
+ * @throws `ConvexError({ code: "TERMINAL_STATE" })` when the message is terminal.
80
+ * @throws `ConvexError({ code: "INVALID_TRANSITION" })` when the message is
81
+ * already `sending` (claimed by another sender).
82
+ */
83
+ export const markSending = mutation({
84
+ args: { messageId: v.string() },
85
+ returns: v.object({ attempts: v.number() }),
86
+ handler: async (ctx, args) => {
87
+ const msg = await ctx.db
88
+ .query("messages")
89
+ .withIndex("by_message_id", (q) => q.eq("messageId", args.messageId))
90
+ .unique();
91
+ if (msg === null) {
92
+ throw new ConvexError({
93
+ code: "NOT_FOUND",
94
+ message: `message "${args.messageId}" not found`,
95
+ });
96
+ }
97
+ if (msg.status === "sent" || msg.status === "failed") {
98
+ throw new ConvexError({
99
+ code: "TERMINAL_STATE",
100
+ message: `message "${args.messageId}" is already ${msg.status} and cannot transition`,
101
+ });
102
+ }
103
+ if (msg.status === "sending") {
104
+ throw new ConvexError({
105
+ code: "INVALID_TRANSITION",
106
+ message: `message "${args.messageId}" is already sending`,
107
+ });
108
+ }
109
+ const attempts = msg.attempts + 1;
110
+ await ctx.db.patch(msg._id, {
111
+ status: "sending",
112
+ attempts,
113
+ updatedAt: Date.now(),
114
+ });
115
+ return { attempts };
116
+ },
117
+ });
118
+ /**
119
+ * Record a successful send — the message moves to terminal `sent`, recording the
120
+ * transport's own `providerId` (its message handle, opaque to the component) and
121
+ * clearing any prior `error`. Idempotent against a replayed callback: re-marking
122
+ * an already-`sent` message is a no-op (the recorded `providerId` is preserved).
123
+ * Any other terminal state (`failed`) is rejected — a `failed` message is final.
124
+ *
125
+ * @throws `ConvexError({ code: "NOT_FOUND" })` when no message has `messageId`.
126
+ * @throws `ConvexError({ code: "TERMINAL_STATE" })` when the message is already
127
+ * `failed` (a final state may not be transitioned out of).
128
+ */
129
+ export const markSent = mutation({
130
+ args: { messageId: v.string(), providerId: v.optional(v.string()) },
131
+ returns: v.null(),
132
+ handler: async (ctx, args) => {
133
+ const msg = await ctx.db
134
+ .query("messages")
135
+ .withIndex("by_message_id", (q) => q.eq("messageId", args.messageId))
136
+ .unique();
137
+ if (msg === null) {
138
+ throw new ConvexError({
139
+ code: "NOT_FOUND",
140
+ message: `message "${args.messageId}" not found`,
141
+ });
142
+ }
143
+ if (msg.status === "sent") {
144
+ return null;
145
+ }
146
+ if (msg.status === "failed") {
147
+ throw new ConvexError({
148
+ code: "TERMINAL_STATE",
149
+ message: `message "${args.messageId}" is already failed and cannot transition`,
150
+ });
151
+ }
152
+ await ctx.db.patch(msg._id, {
153
+ status: "sent",
154
+ providerId: args.providerId,
155
+ error: undefined,
156
+ updatedAt: Date.now(),
157
+ });
158
+ return null;
159
+ },
160
+ });
161
+ /**
162
+ * Record a failed send attempt, recording the host-supplied `error`. The outcome
163
+ * depends on the retry budget: if `attempts < maxAttempts` the message returns to
164
+ * `queued` for another attempt (`retried: true`); once attempts are exhausted it
165
+ * lands in terminal `failed` (`retried: false`). The host's backoff scheduler
166
+ * re-claims a re-queued message when ready. An already-`sent` message is rejected
167
+ * (it succeeded); an already-`failed` message is rejected (it is terminal).
168
+ *
169
+ * @throws `ConvexError({ code: "NOT_FOUND" })` when no message has `messageId`.
170
+ * @throws `ConvexError({ code: "TERMINAL_STATE" })` when the message is already
171
+ * `sent` or `failed`.
172
+ */
173
+ export const markFailed = mutation({
174
+ args: { messageId: v.string(), error: v.optional(v.string()) },
175
+ returns: v.object({ status: v.union(v.literal("queued"), v.literal("failed")), retried: v.boolean() }),
176
+ handler: async (ctx, args) => {
177
+ const msg = await ctx.db
178
+ .query("messages")
179
+ .withIndex("by_message_id", (q) => q.eq("messageId", args.messageId))
180
+ .unique();
181
+ if (msg === null) {
182
+ throw new ConvexError({
183
+ code: "NOT_FOUND",
184
+ message: `message "${args.messageId}" not found`,
185
+ });
186
+ }
187
+ if (msg.status === "sent" || msg.status === "failed") {
188
+ throw new ConvexError({
189
+ code: "TERMINAL_STATE",
190
+ message: `message "${args.messageId}" is already ${msg.status} and cannot transition`,
191
+ });
192
+ }
193
+ const retried = msg.attempts < msg.maxAttempts;
194
+ const status = retried ? "queued" : "failed";
195
+ await ctx.db.patch(msg._id, {
196
+ status,
197
+ error: args.error,
198
+ updatedAt: Date.now(),
199
+ });
200
+ return { status, retried };
201
+ },
202
+ });
203
+ /**
204
+ * Delete up to `batch` terminal messages whose `updatedAt < before`, oldest first
205
+ * — `sent` then `failed`, each via the `by_status_updated` index. `before`
206
+ * defaults to the server clock (`Date.now()`) when omitted, so the built-in cron
207
+ * sweeps exactly the messages terminal-and-stale as of the run. If a full batch
208
+ * was removed there may be more, so the sweep self-reschedules through
209
+ * `ctx.scheduler` until a short batch signals the tail is clean. Idempotent: only
210
+ * ever removes already-terminal, past-retention rows. Queued and sending messages
211
+ * are never pruned.
212
+ */
213
+ export const prune = mutation({
214
+ args: { before: v.optional(v.number()), batch: v.number() },
215
+ returns: v.number(),
216
+ handler: async (ctx, args) => {
217
+ const before = args.before ?? Date.now();
218
+ // Both terminal states, queried explicitly (no query-in-loop): sent first,
219
+ // then failed for whatever batch budget remains.
220
+ const sent = await ctx.db
221
+ .query("messages")
222
+ .withIndex("by_status_updated", (q) => q.eq("status", "sent").lt("updatedAt", before))
223
+ .take(args.batch);
224
+ const failed = sent.length < args.batch
225
+ ? await ctx.db
226
+ .query("messages")
227
+ .withIndex("by_status_updated", (q) => q.eq("status", "failed").lt("updatedAt", before))
228
+ .take(args.batch - sent.length)
229
+ : [];
230
+ for (const row of [...sent, ...failed]) {
231
+ await ctx.db.delete(row._id);
232
+ }
233
+ const removed = sent.length + failed.length;
234
+ if (removed === args.batch) {
235
+ await ctx.scheduler.runAfter(0, api.mutations.prune, {
236
+ before,
237
+ batch: args.batch,
238
+ });
239
+ }
240
+ return removed;
241
+ },
242
+ });
243
+ //# sourceMappingURL=mutations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mutations.js","sourceRoot":"","sources":["../../src/component/mutations.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ /** The current envelope for one message, or `null` if no such id is held. */
2
+ export declare const get: import("convex/server").RegisteredQuery<"public", {
3
+ messageId: string;
4
+ }, Promise<{
5
+ messageId: string;
6
+ to: string;
7
+ from: string;
8
+ transport: string;
9
+ status: "queued" | "sending" | "sent" | "failed";
10
+ payload: any;
11
+ subjectRef: string | undefined;
12
+ idempotencyKey: string | undefined;
13
+ providerId: string | undefined;
14
+ attempts: number;
15
+ maxAttempts: number;
16
+ error: string | undefined;
17
+ createdAt: number;
18
+ updatedAt: number;
19
+ } | null>>;
20
+ /**
21
+ * Page messages in one `status`, oldest first via the `by_status` index. Takes
22
+ * the standard Convex `paginationOpts` and returns the standard paginated
23
+ * envelope (`page`, `isDone`, `continueCursor`) so the host can poll the
24
+ * `queued` backlog to flush, watch `sending`, or audit a terminal `sent`/`failed`
25
+ * history reactively.
26
+ */
27
+ export declare const listByStatus: import("convex/server").RegisteredQuery<"public", {
28
+ status: "queued" | "sending" | "sent" | "failed";
29
+ paginationOpts: {
30
+ id?: number;
31
+ endCursor?: string | null;
32
+ maximumRowsRead?: number;
33
+ maximumBytesRead?: number;
34
+ numItems: number;
35
+ cursor: string | null;
36
+ };
37
+ }, Promise<{
38
+ page: {
39
+ messageId: string;
40
+ to: string;
41
+ from: string;
42
+ transport: string;
43
+ status: "queued" | "sending" | "sent" | "failed";
44
+ payload: any;
45
+ subjectRef: string | undefined;
46
+ idempotencyKey: string | undefined;
47
+ providerId: string | undefined;
48
+ attempts: number;
49
+ maxAttempts: number;
50
+ error: string | undefined;
51
+ createdAt: number;
52
+ updatedAt: number;
53
+ }[];
54
+ isDone: boolean;
55
+ continueCursor: import("convex/server").Cursor;
56
+ splitCursor?: import("convex/server").Cursor | null;
57
+ pageStatus?: "SplitRecommended" | "SplitRequired" | null;
58
+ }>>;
59
+ //# sourceMappingURL=queries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../../src/component/queries.ts"],"names":[],"mappings":"AA0BA,6EAA6E;AAC7E,eAAO,MAAM,GAAG;;;;;;;;;;;;;;;;;UAUd,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuBvB,CAAC"}
@@ -0,0 +1,61 @@
1
+ import { v } from "convex/values";
2
+ import { paginationOptsValidator } from "convex/server";
3
+ import { query } from "./_generated/server";
4
+ import { messageStatus, messageView } from "./validators";
5
+ /** Project a stored message row to its public view (drops internal fields). */
6
+ function view(msg) {
7
+ return {
8
+ messageId: msg.messageId,
9
+ to: msg.to,
10
+ from: msg.from,
11
+ transport: msg.transport,
12
+ status: msg.status,
13
+ payload: msg.payload,
14
+ subjectRef: msg.subjectRef,
15
+ idempotencyKey: msg.idempotencyKey,
16
+ providerId: msg.providerId,
17
+ attempts: msg.attempts,
18
+ maxAttempts: msg.maxAttempts,
19
+ error: msg.error,
20
+ createdAt: msg.createdAt,
21
+ updatedAt: msg.updatedAt,
22
+ };
23
+ }
24
+ /** The current envelope for one message, or `null` if no such id is held. */
25
+ export const get = query({
26
+ args: { messageId: v.string() },
27
+ returns: v.union(v.null(), messageView),
28
+ handler: async (ctx, args) => {
29
+ const msg = await ctx.db
30
+ .query("messages")
31
+ .withIndex("by_message_id", (q) => q.eq("messageId", args.messageId))
32
+ .unique();
33
+ return msg === null ? null : view(msg);
34
+ },
35
+ });
36
+ /**
37
+ * Page messages in one `status`, oldest first via the `by_status` index. Takes
38
+ * the standard Convex `paginationOpts` and returns the standard paginated
39
+ * envelope (`page`, `isDone`, `continueCursor`) so the host can poll the
40
+ * `queued` backlog to flush, watch `sending`, or audit a terminal `sent`/`failed`
41
+ * history reactively.
42
+ */
43
+ export const listByStatus = query({
44
+ args: { status: messageStatus, paginationOpts: paginationOptsValidator },
45
+ returns: v.object({
46
+ page: v.array(messageView),
47
+ isDone: v.boolean(),
48
+ continueCursor: v.string(),
49
+ splitCursor: v.optional(v.union(v.string(), v.null())),
50
+ pageStatus: v.optional(v.union(v.literal("SplitRecommended"), v.literal("SplitRequired"), v.null())),
51
+ }),
52
+ handler: async (ctx, args) => {
53
+ const result = await ctx.db
54
+ .query("messages")
55
+ .withIndex("by_status", (q) => q.eq("status", args.status))
56
+ .order("asc")
57
+ .paginate(args.paginationOpts);
58
+ return { ...result, page: result.page.map(view) };
59
+ },
60
+ });
61
+ //# sourceMappingURL=queries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queries.js","sourceRoot":"","sources":["../../src/component/queries.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Sandboxed table — the outbound-email queue's own concern. A `messageId` is a
3
+ * host-opaque string that uniquely names one queued message; `to`/`from` are the
4
+ * opaque envelope addresses; `transport` names the host-configured adapter (never
5
+ * a baked-in vendor); `status` tracks the lifecycle. `payload` carries the opaque
6
+ * rendered message host data (never inspected). `subjectRef` is an opaque host
7
+ * ref (e.g. the addressee subject) for subject-centric listing; `idempotencyKey`
8
+ * deduplicates double-enqueue; `attempts`/`maxAttempts` drive retry-vs-terminal.
9
+ *
10
+ * Indexes: `by_message_id` (lookup), `by_idempotency_key` (dedup), `by_status`
11
+ * (poll a status queue oldest-first), `by_subject` (subject-centric listing), and
12
+ * `by_status_updated` (retention sweep — prune terminal rows oldest-first).
13
+ */
14
+ declare const _default: import("convex/server").SchemaDefinition<{
15
+ messages: import("convex/server").TableDefinition<import("convex/values").VObject<{
16
+ payload?: any;
17
+ subjectRef?: string | undefined;
18
+ idempotencyKey?: string | undefined;
19
+ providerId?: string | undefined;
20
+ error?: string | undefined;
21
+ messageId: string;
22
+ to: string;
23
+ from: string;
24
+ transport: string;
25
+ maxAttempts: number;
26
+ status: "queued" | "sending" | "sent" | "failed";
27
+ attempts: number;
28
+ createdAt: number;
29
+ updatedAt: number;
30
+ }, {
31
+ messageId: import("convex/values").VString<string, "required">;
32
+ to: import("convex/values").VString<string, "required">;
33
+ from: import("convex/values").VString<string, "required">;
34
+ transport: import("convex/values").VString<string, "required">;
35
+ status: import("convex/values").VUnion<"queued" | "sending" | "sent" | "failed", [import("convex/values").VLiteral<"queued", "required">, import("convex/values").VLiteral<"sending", "required">, import("convex/values").VLiteral<"sent", "required">, import("convex/values").VLiteral<"failed", "required">], "required", never>;
36
+ payload: import("convex/values").VAny<any, "optional", string>;
37
+ subjectRef: import("convex/values").VString<string | undefined, "optional">;
38
+ idempotencyKey: import("convex/values").VString<string | undefined, "optional">;
39
+ providerId: import("convex/values").VString<string | undefined, "optional">;
40
+ attempts: import("convex/values").VFloat64<number, "required">;
41
+ maxAttempts: import("convex/values").VFloat64<number, "required">;
42
+ error: import("convex/values").VString<string | undefined, "optional">;
43
+ createdAt: import("convex/values").VFloat64<number, "required">;
44
+ updatedAt: import("convex/values").VFloat64<number, "required">;
45
+ }, "required", "messageId" | "to" | "from" | "transport" | "payload" | "subjectRef" | "idempotencyKey" | "maxAttempts" | "providerId" | "error" | "status" | "attempts" | "createdAt" | "updatedAt" | `payload.${string}`>, {
46
+ by_message_id: ["messageId", "_creationTime"];
47
+ by_idempotency_key: ["idempotencyKey", "_creationTime"];
48
+ by_status: ["status", "createdAt", "_creationTime"];
49
+ by_subject: ["subjectRef", "createdAt", "_creationTime"];
50
+ by_status_updated: ["status", "updatedAt", "_creationTime"];
51
+ }, {}, {}>;
52
+ }, true>;
53
+ export default _default;
54
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/component/schema.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACH,wBAsBG"}
@@ -0,0 +1,40 @@
1
+ import { defineSchema, defineTable } from "convex/server";
2
+ import { v } from "convex/values";
3
+ import { jsonValue, messageStatus } from "./validators";
4
+ /**
5
+ * Sandboxed table — the outbound-email queue's own concern. A `messageId` is a
6
+ * host-opaque string that uniquely names one queued message; `to`/`from` are the
7
+ * opaque envelope addresses; `transport` names the host-configured adapter (never
8
+ * a baked-in vendor); `status` tracks the lifecycle. `payload` carries the opaque
9
+ * rendered message host data (never inspected). `subjectRef` is an opaque host
10
+ * ref (e.g. the addressee subject) for subject-centric listing; `idempotencyKey`
11
+ * deduplicates double-enqueue; `attempts`/`maxAttempts` drive retry-vs-terminal.
12
+ *
13
+ * Indexes: `by_message_id` (lookup), `by_idempotency_key` (dedup), `by_status`
14
+ * (poll a status queue oldest-first), `by_subject` (subject-centric listing), and
15
+ * `by_status_updated` (retention sweep — prune terminal rows oldest-first).
16
+ */
17
+ export default defineSchema({
18
+ messages: defineTable({
19
+ messageId: v.string(),
20
+ to: v.string(),
21
+ from: v.string(),
22
+ transport: v.string(),
23
+ status: messageStatus,
24
+ payload: v.optional(jsonValue),
25
+ subjectRef: v.optional(v.string()),
26
+ idempotencyKey: v.optional(v.string()),
27
+ providerId: v.optional(v.string()),
28
+ attempts: v.number(),
29
+ maxAttempts: v.number(),
30
+ error: v.optional(v.string()),
31
+ createdAt: v.number(),
32
+ updatedAt: v.number(),
33
+ })
34
+ .index("by_message_id", ["messageId"])
35
+ .index("by_idempotency_key", ["idempotencyKey"])
36
+ .index("by_status", ["status", "createdAt"])
37
+ .index("by_subject", ["subjectRef", "createdAt"])
38
+ .index("by_status_updated", ["status", "updatedAt"]),
39
+ });
40
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/component/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,CAAC,EAAE,MAAM,eAAe,CAAC;AAClC,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAExD;;;;;;;;;;;;GAYG;AACH,eAAe,YAAY,CAAC;IAC1B,QAAQ,EAAE,WAAW,CAAC;QACpB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;QACrB,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE;QACd,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;QAChB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;QACrB,MAAM,EAAE,aAAa;QACrB,OAAO,EAAE,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC;QAC9B,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAClC,cAAc,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QACtC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAClC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;QACpB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;QACvB,KAAK,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAC7B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;QACrB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;KACtB,CAAC;SACC,KAAK,CAAC,eAAe,EAAE,CAAC,WAAW,CAAC,CAAC;SACrC,KAAK,CAAC,oBAAoB,EAAE,CAAC,gBAAgB,CAAC,CAAC;SAC/C,KAAK,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;SAC3C,KAAK,CAAC,YAAY,EAAE,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;SAChD,KAAK,CAAC,mBAAmB,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;CACvD,CAAC,CAAC"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Opaque host-owned data stored on a message — its `payload` (rendered body,
3
+ * template name + data, headers, attachment refs — whatever the host's transport
4
+ * needs to send). The component never inspects it; it is last-resort arbitrary
5
+ * data, aliased here rather than left bare in function signatures. The host
6
+ * narrows it at the {@link Email} client boundary via an optional
7
+ * `payloadValidator` parser.
8
+ *
9
+ * This is the single documented `v.any()` escape hatch in the component; the lint
10
+ * rule `convex-rules/no-bare-v-any` is satisfied by routing every arbitrary host
11
+ * payload through this alias instead of a bare `v.any()`.
12
+ */
13
+ export declare const jsonValue: import("convex/values").VAny<any, "required", string>;
14
+ /** The four lifecycle states a message moves through. */
15
+ export declare const messageStatus: import("convex/values").VUnion<"queued" | "sending" | "sent" | "failed", [import("convex/values").VLiteral<"queued", "required">, import("convex/values").VLiteral<"sending", "required">, import("convex/values").VLiteral<"sent", "required">, import("convex/values").VLiteral<"failed", "required">], "required", never>;
16
+ /**
17
+ * Public projection of a message returned by {@link get}. `payload` is opaque
18
+ * host data; `transport` names the host-configured adapter the message is routed
19
+ * through (never a baked-in vendor); `providerId` is the transport's own message
20
+ * handle recorded on a successful send; `error` is the host-supplied failure
21
+ * reason recorded on a failed attempt.
22
+ */
23
+ export declare const messageView: import("convex/values").VObject<{
24
+ payload?: any;
25
+ subjectRef?: string | undefined;
26
+ idempotencyKey?: string | undefined;
27
+ providerId?: string | undefined;
28
+ error?: string | undefined;
29
+ messageId: string;
30
+ to: string;
31
+ from: string;
32
+ transport: string;
33
+ maxAttempts: number;
34
+ status: "queued" | "sending" | "sent" | "failed";
35
+ attempts: number;
36
+ createdAt: number;
37
+ updatedAt: number;
38
+ }, {
39
+ messageId: import("convex/values").VString<string, "required">;
40
+ to: import("convex/values").VString<string, "required">;
41
+ from: import("convex/values").VString<string, "required">;
42
+ transport: import("convex/values").VString<string, "required">;
43
+ status: import("convex/values").VUnion<"queued" | "sending" | "sent" | "failed", [import("convex/values").VLiteral<"queued", "required">, import("convex/values").VLiteral<"sending", "required">, import("convex/values").VLiteral<"sent", "required">, import("convex/values").VLiteral<"failed", "required">], "required", never>;
44
+ payload: import("convex/values").VAny<any, "optional", string>;
45
+ subjectRef: import("convex/values").VString<string | undefined, "optional">;
46
+ idempotencyKey: import("convex/values").VString<string | undefined, "optional">;
47
+ providerId: import("convex/values").VString<string | undefined, "optional">;
48
+ attempts: import("convex/values").VFloat64<number, "required">;
49
+ maxAttempts: import("convex/values").VFloat64<number, "required">;
50
+ error: import("convex/values").VString<string | undefined, "optional">;
51
+ createdAt: import("convex/values").VFloat64<number, "required">;
52
+ updatedAt: import("convex/values").VFloat64<number, "required">;
53
+ }, "required", "messageId" | "to" | "from" | "transport" | "payload" | "subjectRef" | "idempotencyKey" | "maxAttempts" | "providerId" | "error" | "status" | "attempts" | "createdAt" | "updatedAt" | `payload.${string}`>;
54
+ //# sourceMappingURL=validators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../../src/component/validators.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,SAAS,uDAAU,CAAC;AAEjC,yDAAyD;AACzD,eAAO,MAAM,aAAa,8TAKzB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0NAetB,CAAC"}
@@ -0,0 +1,40 @@
1
+ import { v } from "convex/values";
2
+ /**
3
+ * Opaque host-owned data stored on a message — its `payload` (rendered body,
4
+ * template name + data, headers, attachment refs — whatever the host's transport
5
+ * needs to send). The component never inspects it; it is last-resort arbitrary
6
+ * data, aliased here rather than left bare in function signatures. The host
7
+ * narrows it at the {@link Email} client boundary via an optional
8
+ * `payloadValidator` parser.
9
+ *
10
+ * This is the single documented `v.any()` escape hatch in the component; the lint
11
+ * rule `convex-rules/no-bare-v-any` is satisfied by routing every arbitrary host
12
+ * payload through this alias instead of a bare `v.any()`.
13
+ */
14
+ export const jsonValue = v.any();
15
+ /** The four lifecycle states a message moves through. */
16
+ export const messageStatus = v.union(v.literal("queued"), v.literal("sending"), v.literal("sent"), v.literal("failed"));
17
+ /**
18
+ * Public projection of a message returned by {@link get}. `payload` is opaque
19
+ * host data; `transport` names the host-configured adapter the message is routed
20
+ * through (never a baked-in vendor); `providerId` is the transport's own message
21
+ * handle recorded on a successful send; `error` is the host-supplied failure
22
+ * reason recorded on a failed attempt.
23
+ */
24
+ export const messageView = v.object({
25
+ messageId: v.string(),
26
+ to: v.string(),
27
+ from: v.string(),
28
+ transport: v.string(),
29
+ status: messageStatus,
30
+ payload: v.optional(jsonValue),
31
+ subjectRef: v.optional(v.string()),
32
+ idempotencyKey: v.optional(v.string()),
33
+ providerId: v.optional(v.string()),
34
+ attempts: v.number(),
35
+ maxAttempts: v.number(),
36
+ error: v.optional(v.string()),
37
+ createdAt: v.number(),
38
+ updatedAt: v.number(),
39
+ });
40
+ //# sourceMappingURL=validators.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validators.js","sourceRoot":"","sources":["../../src/component/validators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,eAAe,CAAC;AAElC;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;AAEjC,yDAAyD;AACzD,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAClC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,EACnB,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,EACpB,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,EACjB,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CACpB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE;IACd,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,MAAM,EAAE,aAAa;IACrB,OAAO,EAAE,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC;IAC9B,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAClC,cAAc,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IACtC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAClC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;IACvB,KAAK,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAC7B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;CACtB,CAAC,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The optional generic-JMAP transport adapter — a `./jmap` entry the host imports
3
+ * into its OWN Convex action to actually send queued messages over a JMAP server's
4
+ * HTTP API (Stalwart, Fastmail, Cyrus, any JMAP server). It is NOT part of the
5
+ * sandboxed component: the component records intent + status and never sends.
6
+ *
7
+ * Unlike `./smtp`, this layer is **pure** and dependency-free: it drives an
8
+ * injected `fetch` (the host's runtime `fetch`), so it needs no `nodemailer`, no
9
+ * `"use node"` action (JMAP is plain HTTP — `fetch` runs in a normal Convex
10
+ * action), and is 100%-covered with a fake `fetch` (no excluded wrapper).
11
+ *
12
+ * Tree-shake boundary: a backend-only consumer importing `@vllnt/convex-email`
13
+ * (the `.` entry) pulls ZERO JMAP code. Importing this `./jmap` entry is the
14
+ * explicit opt-in.
15
+ *
16
+ * JMAP is a protocol (RFC 8620/8621), not a vendor — Stalwart is one configured
17
+ * server (`{ endpoint, token }`), never baked in, so this is `./jmap`, never
18
+ * `./stalwart`.
19
+ */
20
+ export { validateJmapConfig, buildEmailCreate, buildSubmitRequest, parseSubmitResponse, sendViaJmap, discoverJmapSession, createJmapSender, } from "./send.js";
21
+ export type { JmapConfig, JmapMessage, JmapSendResult, JmapSender, JmapFetch, JmapRequestInit, JmapResponse, JmapDiscoverOptions, } from "./types.js";
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/jmap/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,WAAW,EACX,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,WAAW,CAAC;AACnB,YAAY,EACV,UAAU,EACV,WAAW,EACX,cAAc,EACd,UAAU,EACV,SAAS,EACT,eAAe,EACf,YAAY,EACZ,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The optional generic-JMAP transport adapter — a `./jmap` entry the host imports
3
+ * into its OWN Convex action to actually send queued messages over a JMAP server's
4
+ * HTTP API (Stalwart, Fastmail, Cyrus, any JMAP server). It is NOT part of the
5
+ * sandboxed component: the component records intent + status and never sends.
6
+ *
7
+ * Unlike `./smtp`, this layer is **pure** and dependency-free: it drives an
8
+ * injected `fetch` (the host's runtime `fetch`), so it needs no `nodemailer`, no
9
+ * `"use node"` action (JMAP is plain HTTP — `fetch` runs in a normal Convex
10
+ * action), and is 100%-covered with a fake `fetch` (no excluded wrapper).
11
+ *
12
+ * Tree-shake boundary: a backend-only consumer importing `@vllnt/convex-email`
13
+ * (the `.` entry) pulls ZERO JMAP code. Importing this `./jmap` entry is the
14
+ * explicit opt-in.
15
+ *
16
+ * JMAP is a protocol (RFC 8620/8621), not a vendor — Stalwart is one configured
17
+ * server (`{ endpoint, token }`), never baked in, so this is `./jmap`, never
18
+ * `./stalwart`.
19
+ */
20
+ export { validateJmapConfig, buildEmailCreate, buildSubmitRequest, parseSubmitResponse, sendViaJmap, discoverJmapSession, createJmapSender, } from "./send.js";
21
+ //# sourceMappingURL=index.js.map