@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.
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/client/index.d.ts +174 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +151 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/types.d.ts +84 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +3 -0
- package/dist/client/types.js.map +1 -0
- package/dist/component/_generated/api.d.ts +40 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +110 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +7 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/crons.d.ts +16 -0
- package/dist/component/crons.d.ts.map +1 -0
- package/dist/component/crons.js +19 -0
- package/dist/component/crons.js.map +1 -0
- package/dist/component/mutations.d.ts +95 -0
- package/dist/component/mutations.d.ts.map +1 -0
- package/dist/component/mutations.js +243 -0
- package/dist/component/mutations.js.map +1 -0
- package/dist/component/queries.d.ts +59 -0
- package/dist/component/queries.d.ts.map +1 -0
- package/dist/component/queries.js +61 -0
- package/dist/component/queries.js.map +1 -0
- package/dist/component/schema.d.ts +54 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +40 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/validators.d.ts +54 -0
- package/dist/component/validators.d.ts.map +1 -0
- package/dist/component/validators.js +40 -0
- package/dist/component/validators.js.map +1 -0
- package/dist/jmap/index.d.ts +22 -0
- package/dist/jmap/index.d.ts.map +1 -0
- package/dist/jmap/index.js +21 -0
- package/dist/jmap/index.js.map +1 -0
- package/dist/jmap/send.d.ts +125 -0
- package/dist/jmap/send.d.ts.map +1 -0
- package/dist/jmap/send.js +418 -0
- package/dist/jmap/send.js.map +1 -0
- package/dist/jmap/types.d.ts +107 -0
- package/dist/jmap/types.d.ts.map +1 -0
- package/dist/jmap/types.js +16 -0
- package/dist/jmap/types.js.map +1 -0
- package/dist/shared.d.ts +32 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +33 -0
- package/dist/shared.js.map +1 -0
- package/dist/smtp/index.d.ts +22 -0
- package/dist/smtp/index.d.ts.map +1 -0
- package/dist/smtp/index.js +21 -0
- package/dist/smtp/index.js.map +1 -0
- package/dist/smtp/send.d.ts +51 -0
- package/dist/smtp/send.d.ts.map +1 -0
- package/dist/smtp/send.js +124 -0
- package/dist/smtp/send.js.map +1 -0
- package/dist/smtp/transport.d.ts +43 -0
- package/dist/smtp/transport.d.ts.map +1 -0
- package/dist/smtp/transport.js +55 -0
- package/dist/smtp/transport.js.map +1 -0
- package/dist/smtp/types.d.ts +122 -0
- package/dist/smtp/types.d.ts.map +1 -0
- package/dist/smtp/types.js +9 -0
- package/dist/smtp/types.js.map +1 -0
- package/package.json +118 -0
- package/src/client/index.ts +312 -0
- package/src/client/types.ts +90 -0
- package/src/component/_generated/api.ts +56 -0
- package/src/component/_generated/component.ts +134 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/convex.config.ts +9 -0
- package/src/component/crons.ts +23 -0
- package/src/component/mutations.ts +262 -0
- package/src/component/queries.ts +70 -0
- package/src/component/schema.ts +40 -0
- package/src/component/validators.ts +47 -0
- package/src/jmap/index.ts +39 -0
- package/src/jmap/send.test.ts +565 -0
- package/src/jmap/send.ts +502 -0
- package/src/jmap/types.ts +117 -0
- package/src/shared.ts +41 -0
- package/src/smtp/index.ts +30 -0
- package/src/smtp/send.test.ts +240 -0
- package/src/smtp/send.ts +154 -0
- package/src/smtp/transport.ts +58 -0
- package/src/smtp/types.ts +124 -0
- package/src/test.ts +12 -0
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
import type { Doc } from "./_generated/dataModel";
|
|
6
|
+
|
|
7
|
+
/** Project a stored message row to its public view (drops internal fields). */
|
|
8
|
+
function view(msg: Doc<"messages">) {
|
|
9
|
+
return {
|
|
10
|
+
messageId: msg.messageId,
|
|
11
|
+
to: msg.to,
|
|
12
|
+
from: msg.from,
|
|
13
|
+
transport: msg.transport,
|
|
14
|
+
status: msg.status,
|
|
15
|
+
payload: msg.payload,
|
|
16
|
+
subjectRef: msg.subjectRef,
|
|
17
|
+
idempotencyKey: msg.idempotencyKey,
|
|
18
|
+
providerId: msg.providerId,
|
|
19
|
+
attempts: msg.attempts,
|
|
20
|
+
maxAttempts: msg.maxAttempts,
|
|
21
|
+
error: msg.error,
|
|
22
|
+
createdAt: msg.createdAt,
|
|
23
|
+
updatedAt: msg.updatedAt,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The current envelope for one message, or `null` if no such id is held. */
|
|
28
|
+
export const get = query({
|
|
29
|
+
args: { messageId: v.string() },
|
|
30
|
+
returns: v.union(v.null(), messageView),
|
|
31
|
+
handler: async (ctx, args) => {
|
|
32
|
+
const msg = await ctx.db
|
|
33
|
+
.query("messages")
|
|
34
|
+
.withIndex("by_message_id", (q) => q.eq("messageId", args.messageId))
|
|
35
|
+
.unique();
|
|
36
|
+
return msg === null ? null : view(msg);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Page messages in one `status`, oldest first via the `by_status` index. Takes
|
|
42
|
+
* the standard Convex `paginationOpts` and returns the standard paginated
|
|
43
|
+
* envelope (`page`, `isDone`, `continueCursor`) so the host can poll the
|
|
44
|
+
* `queued` backlog to flush, watch `sending`, or audit a terminal `sent`/`failed`
|
|
45
|
+
* history reactively.
|
|
46
|
+
*/
|
|
47
|
+
export const listByStatus = query({
|
|
48
|
+
args: { status: messageStatus, paginationOpts: paginationOptsValidator },
|
|
49
|
+
returns: v.object({
|
|
50
|
+
page: v.array(messageView),
|
|
51
|
+
isDone: v.boolean(),
|
|
52
|
+
continueCursor: v.string(),
|
|
53
|
+
splitCursor: v.optional(v.union(v.string(), v.null())),
|
|
54
|
+
pageStatus: v.optional(
|
|
55
|
+
v.union(
|
|
56
|
+
v.literal("SplitRecommended"),
|
|
57
|
+
v.literal("SplitRequired"),
|
|
58
|
+
v.null(),
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
}),
|
|
62
|
+
handler: async (ctx, args) => {
|
|
63
|
+
const result = await ctx.db
|
|
64
|
+
.query("messages")
|
|
65
|
+
.withIndex("by_status", (q) => q.eq("status", args.status))
|
|
66
|
+
.order("asc")
|
|
67
|
+
.paginate(args.paginationOpts);
|
|
68
|
+
return { ...result, page: result.page.map(view) };
|
|
69
|
+
},
|
|
70
|
+
});
|
|
@@ -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
|
+
/**
|
|
6
|
+
* Sandboxed table — the outbound-email queue's own concern. A `messageId` is a
|
|
7
|
+
* host-opaque string that uniquely names one queued message; `to`/`from` are the
|
|
8
|
+
* opaque envelope addresses; `transport` names the host-configured adapter (never
|
|
9
|
+
* a baked-in vendor); `status` tracks the lifecycle. `payload` carries the opaque
|
|
10
|
+
* rendered message host data (never inspected). `subjectRef` is an opaque host
|
|
11
|
+
* ref (e.g. the addressee subject) for subject-centric listing; `idempotencyKey`
|
|
12
|
+
* deduplicates double-enqueue; `attempts`/`maxAttempts` drive retry-vs-terminal.
|
|
13
|
+
*
|
|
14
|
+
* Indexes: `by_message_id` (lookup), `by_idempotency_key` (dedup), `by_status`
|
|
15
|
+
* (poll a status queue oldest-first), `by_subject` (subject-centric listing), and
|
|
16
|
+
* `by_status_updated` (retention sweep — prune terminal rows oldest-first).
|
|
17
|
+
*/
|
|
18
|
+
export default defineSchema({
|
|
19
|
+
messages: defineTable({
|
|
20
|
+
messageId: v.string(),
|
|
21
|
+
to: v.string(),
|
|
22
|
+
from: v.string(),
|
|
23
|
+
transport: v.string(),
|
|
24
|
+
status: messageStatus,
|
|
25
|
+
payload: v.optional(jsonValue),
|
|
26
|
+
subjectRef: v.optional(v.string()),
|
|
27
|
+
idempotencyKey: v.optional(v.string()),
|
|
28
|
+
providerId: v.optional(v.string()),
|
|
29
|
+
attempts: v.number(),
|
|
30
|
+
maxAttempts: v.number(),
|
|
31
|
+
error: v.optional(v.string()),
|
|
32
|
+
createdAt: v.number(),
|
|
33
|
+
updatedAt: v.number(),
|
|
34
|
+
})
|
|
35
|
+
.index("by_message_id", ["messageId"])
|
|
36
|
+
.index("by_idempotency_key", ["idempotencyKey"])
|
|
37
|
+
.index("by_status", ["status", "createdAt"])
|
|
38
|
+
.index("by_subject", ["subjectRef", "createdAt"])
|
|
39
|
+
.index("by_status_updated", ["status", "updatedAt"]),
|
|
40
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Opaque host-owned data stored on a message — its `payload` (rendered body,
|
|
5
|
+
* template name + data, headers, attachment refs — whatever the host's transport
|
|
6
|
+
* needs to send). The component never inspects it; it is last-resort arbitrary
|
|
7
|
+
* data, aliased here rather than left bare in function signatures. The host
|
|
8
|
+
* narrows it at the {@link Email} client boundary via an optional
|
|
9
|
+
* `payloadValidator` parser.
|
|
10
|
+
*
|
|
11
|
+
* This is the single documented `v.any()` escape hatch in the component; the lint
|
|
12
|
+
* rule `convex-rules/no-bare-v-any` is satisfied by routing every arbitrary host
|
|
13
|
+
* payload through this alias instead of a bare `v.any()`.
|
|
14
|
+
*/
|
|
15
|
+
export const jsonValue = v.any();
|
|
16
|
+
|
|
17
|
+
/** The four lifecycle states a message moves through. */
|
|
18
|
+
export const messageStatus = v.union(
|
|
19
|
+
v.literal("queued"),
|
|
20
|
+
v.literal("sending"),
|
|
21
|
+
v.literal("sent"),
|
|
22
|
+
v.literal("failed"),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Public projection of a message returned by {@link get}. `payload` is opaque
|
|
27
|
+
* host data; `transport` names the host-configured adapter the message is routed
|
|
28
|
+
* through (never a baked-in vendor); `providerId` is the transport's own message
|
|
29
|
+
* handle recorded on a successful send; `error` is the host-supplied failure
|
|
30
|
+
* reason recorded on a failed attempt.
|
|
31
|
+
*/
|
|
32
|
+
export const messageView = v.object({
|
|
33
|
+
messageId: v.string(),
|
|
34
|
+
to: v.string(),
|
|
35
|
+
from: v.string(),
|
|
36
|
+
transport: v.string(),
|
|
37
|
+
status: messageStatus,
|
|
38
|
+
payload: v.optional(jsonValue),
|
|
39
|
+
subjectRef: v.optional(v.string()),
|
|
40
|
+
idempotencyKey: v.optional(v.string()),
|
|
41
|
+
providerId: v.optional(v.string()),
|
|
42
|
+
attempts: v.number(),
|
|
43
|
+
maxAttempts: v.number(),
|
|
44
|
+
error: v.optional(v.string()),
|
|
45
|
+
createdAt: v.number(),
|
|
46
|
+
updatedAt: v.number(),
|
|
47
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
|
|
21
|
+
export {
|
|
22
|
+
validateJmapConfig,
|
|
23
|
+
buildEmailCreate,
|
|
24
|
+
buildSubmitRequest,
|
|
25
|
+
parseSubmitResponse,
|
|
26
|
+
sendViaJmap,
|
|
27
|
+
discoverJmapSession,
|
|
28
|
+
createJmapSender,
|
|
29
|
+
} from "./send.js";
|
|
30
|
+
export type {
|
|
31
|
+
JmapConfig,
|
|
32
|
+
JmapMessage,
|
|
33
|
+
JmapSendResult,
|
|
34
|
+
JmapSender,
|
|
35
|
+
JmapFetch,
|
|
36
|
+
JmapRequestInit,
|
|
37
|
+
JmapResponse,
|
|
38
|
+
JmapDiscoverOptions,
|
|
39
|
+
} from "./types.js";
|