@lunora/mail 1.0.0-alpha.5 → 1.0.0-alpha.50
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.md +6 -0
- package/dist/inbound/index.d.mts +209 -70
- package/dist/inbound/index.d.ts +209 -70
- package/dist/inbound/index.mjs +1 -2
- package/dist/index.d.mts +94 -94
- package/dist/index.d.ts +94 -94
- package/dist/index.mjs +1 -7
- package/dist/packem_shared/address-BQ5RUGdT.mjs +3 -0
- package/dist/packem_shared/capture-transport.d-_rIl9W6N.d.mts +117 -0
- package/dist/packem_shared/capture-transport.d-_rIl9W6N.d.ts +117 -0
- package/dist/packem_shared/consumeQueuedSend-8TXPj5o-.mjs +1 -0
- package/dist/packem_shared/createCaptureSink-CE24od5h.mjs +1 -0
- package/dist/packem_shared/createCaptureTransport-CKc6NpZR.mjs +1 -0
- package/dist/packem_shared/createCloudflareTransport-E-UdW8Dc.mjs +1 -0
- package/dist/packem_shared/createInboundEmailHandler-D2i0mEBB.mjs +1 -0
- package/dist/packem_shared/createMailer-LHgKs5yh.mjs +1 -0
- package/dist/packem_shared/createResendTransport-CG9s8iI9.mjs +1 -0
- package/dist/packem_shared/parseInboundEmail-Cxe54g-h.mjs +1 -0
- package/dist/packem_shared/provider-transport-DXmTG_xv.mjs +1 -0
- package/dist/packem_shared/renderEmail-BOUnr6i3.mjs +1 -0
- package/dist/packem_shared/shard-C0aRRoVy.mjs +1 -0
- package/dist/packem_shared/shard.d-DVADjmEJ.d.mts +29 -0
- package/dist/packem_shared/shard.d-DVADjmEJ.d.ts +29 -0
- package/dist/testing.d.mts +9 -9
- package/dist/testing.d.ts +9 -9
- package/dist/testing.mjs +1 -57
- package/package.json +5 -4
- package/dist/packem_shared/address-fkXxLKza.mjs +0 -62
- package/dist/packem_shared/capture-transport.d-ChnhdPO2.d.mts +0 -117
- package/dist/packem_shared/capture-transport.d-ChnhdPO2.d.ts +0 -117
- package/dist/packem_shared/consumeQueuedSend-BEKOdaxU.mjs +0 -75
- package/dist/packem_shared/createCaptureSink-DeihS4LH.mjs +0 -63
- package/dist/packem_shared/createCaptureTransport-Crz_8822.mjs +0 -11
- package/dist/packem_shared/createCloudflareTransport-yHOVEsZv.mjs +0 -26
- package/dist/packem_shared/createInboundEmailHandler-D0uCOrU-.mjs +0 -83
- package/dist/packem_shared/createMailer-oEKPAd4J.mjs +0 -77
- package/dist/packem_shared/createResendTransport-oNIorpzv.mjs +0 -16
- package/dist/packem_shared/parseInboundEmail-Bw9u_1oc.mjs +0 -72
- package/dist/packem_shared/provider-transport-C5CVbjRF.mjs +0 -47
- package/dist/packem_shared/renderEmail-hyS1bpVP.mjs +0 -8
- package/dist/packem_shared/shard-CJ-TvmfT.mjs +0 -13
- package/dist/packem_shared/shard.d-CL2Lmliv.d.mts +0 -39
- package/dist/packem_shared/shard.d-CL2Lmliv.d.ts +0 -39
package/dist/index.d.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { M as MailTransport, L as LunoraMailOptions, a as Mailer, b as MailboxSink, S as SendOptions } from "./packem_shared/capture-transport.d-
|
|
2
|
-
export { type C as CapturedMail, type Q as QueueLike, type c as SendPayload, d as createCaptureTransport } from "./packem_shared/capture-transport.d-
|
|
3
|
-
import { D as DurableObjectJurisdiction } from "./packem_shared/shard.d-
|
|
1
|
+
import { M as MailTransport, L as LunoraMailOptions, a as Mailer, b as MailboxSink, S as SendOptions } from "./packem_shared/capture-transport.d-_rIl9W6N.js";
|
|
2
|
+
export { type C as CapturedMail, type Q as QueueLike, type c as SendPayload, d as createCaptureTransport } from "./packem_shared/capture-transport.d-_rIl9W6N.js";
|
|
3
|
+
import { D as DurableObjectJurisdiction } from "./packem_shared/shard.d-DVADjmEJ.js";
|
|
4
4
|
import { ReactElement } from 'react';
|
|
5
5
|
/**
|
|
6
|
-
* Sends a raw RFC 822 message through a Worker's Email send binding. The Workers
|
|
7
|
-
* runtime owns the binding, so the caller supplies this thin callback — keeping
|
|
8
|
-
* `@lunora/mail` free of a `cloudflare:email` import and unit-testable. Wire it
|
|
9
|
-
* in your project as:
|
|
10
|
-
*
|
|
11
|
-
* ```ts
|
|
12
|
-
* send: async (from, to, raw) => {
|
|
13
|
-
* const { EmailMessage } = await import("cloudflare:email");
|
|
14
|
-
* await env.SEND_EMAIL.send(new EmailMessage(from, to, raw));
|
|
15
|
-
* }
|
|
16
|
-
* ```
|
|
17
|
-
*/
|
|
6
|
+
* Sends a raw RFC 822 message through a Worker's Email send binding. The Workers
|
|
7
|
+
* runtime owns the binding, so the caller supplies this thin callback — keeping
|
|
8
|
+
* `@lunora/mail` free of a `cloudflare:email` import and unit-testable. Wire it
|
|
9
|
+
* in your project as:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* send: async (from, to, raw) => {
|
|
13
|
+
* const { EmailMessage } = await import("cloudflare:email");
|
|
14
|
+
* await env.SEND_EMAIL.send(new EmailMessage(from, to, raw));
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
18
|
type CloudflareSend = (from: string, to: string, raw: string) => Promise<void>;
|
|
19
19
|
interface CloudflareTransportOptions {
|
|
20
20
|
/** Default sender used when a `SendOptions.from` isn't supplied. */
|
|
@@ -23,34 +23,34 @@ interface CloudflareTransportOptions {
|
|
|
23
23
|
send: CloudflareSend;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
* Build the default Cloudflare Email Workers transport via `@visulima/email`.
|
|
27
|
-
* Cloudflare's `send_email` binding is **single-recipient** and only delivers to
|
|
28
|
-
* **verified Email Routing destination addresses**, and the underlying provider
|
|
29
|
-
* rejects any `cc`/`bcc` or a non-single `to` outright. So this enforces a single
|
|
30
|
-
* `to` recipient (throwing a clear error instead of silently dropping the rest)
|
|
31
|
-
* and rejects `cc`/`bcc` rather than forwarding them into a generic "send failed".
|
|
32
|
-
* Multi-recipient transactional mail must fan out one send per recipient at the
|
|
33
|
-
* call site. In dev the capture transport intercepts before this runs, so the
|
|
34
|
-
* verified-address constraint never bites the dev loop.
|
|
35
|
-
*/
|
|
26
|
+
* Build the default Cloudflare Email Workers transport via `@visulima/email`.
|
|
27
|
+
* Cloudflare's `send_email` binding is **single-recipient** and only delivers to
|
|
28
|
+
* **verified Email Routing destination addresses**, and the underlying provider
|
|
29
|
+
* rejects any `cc`/`bcc` or a non-single `to` outright. So this enforces a single
|
|
30
|
+
* `to` recipient (throwing a clear error instead of silently dropping the rest)
|
|
31
|
+
* and rejects `cc`/`bcc` rather than forwarding them into a generic "send failed".
|
|
32
|
+
* Multi-recipient transactional mail must fan out one send per recipient at the
|
|
33
|
+
* call site. In dev the capture transport intercepts before this runs, so the
|
|
34
|
+
* verified-address constraint never bites the dev loop.
|
|
35
|
+
*/
|
|
36
36
|
declare const createCloudflareTransport: (options: CloudflareTransportOptions) => MailTransport;
|
|
37
37
|
/**
|
|
38
|
-
* Create a mailer bound to a transport.
|
|
39
|
-
*
|
|
40
|
-
* SECURITY — recipient policy and HTML content are the caller's responsibility.
|
|
41
|
-
* The mailer fully blocks header/CRLF/comma injection in addresses
|
|
42
|
-
* (`assertSafeAddresses` / `assertSafeHeaderValue`), but it does NOT decide WHO
|
|
43
|
-
* you may send to or WHAT HTML you render.
|
|
44
|
-
*
|
|
45
|
-
* Open relay: derive `to`/`cc`/`bcc` from server-trusted state, never from raw
|
|
46
|
-
* request input, and prefer a fixed/allowlisted `from` — sending to an arbitrary
|
|
47
|
-
* user-supplied address turns your deployment into a spam relay.
|
|
48
|
-
*
|
|
49
|
-
* Template XSS / content injection: treat template HTML like any other HTML sink
|
|
50
|
-
* — never interpolate untrusted data into raw markup (or a
|
|
51
|
-
* `dangerouslySetInnerHTML`-style template) without escaping. The mailer sends
|
|
52
|
-
* whatever HTML you hand it verbatim.
|
|
53
|
-
*/
|
|
38
|
+
* Create a mailer bound to a transport.
|
|
39
|
+
*
|
|
40
|
+
* SECURITY — recipient policy and HTML content are the caller's responsibility.
|
|
41
|
+
* The mailer fully blocks header/CRLF/comma injection in addresses
|
|
42
|
+
* (`assertSafeAddresses` / `assertSafeHeaderValue`), but it does NOT decide WHO
|
|
43
|
+
* you may send to or WHAT HTML you render.
|
|
44
|
+
*
|
|
45
|
+
* Open relay: derive `to`/`cc`/`bcc` from server-trusted state, never from raw
|
|
46
|
+
* request input, and prefer a fixed/allowlisted `from` — sending to an arbitrary
|
|
47
|
+
* user-supplied address turns your deployment into a spam relay.
|
|
48
|
+
*
|
|
49
|
+
* Template XSS / content injection: treat template HTML like any other HTML sink
|
|
50
|
+
* — never interpolate untrusted data into raw markup (or a
|
|
51
|
+
* `dangerouslySetInnerHTML`-style template) without escaping. The mailer sends
|
|
52
|
+
* whatever HTML you hand it verbatim.
|
|
53
|
+
*/
|
|
54
54
|
declare const createMailer: (options: LunoraMailOptions) => Mailer;
|
|
55
55
|
/** A Worker `env` projected as a plain record (vars, secrets, and bindings are `unknown`-valued). */
|
|
56
56
|
type MailEnv = Record<string, unknown>;
|
|
@@ -59,39 +59,39 @@ interface FromEnvOptions {
|
|
|
59
59
|
/** RFC 822 send callback bound to the Worker's `send_email` binding (Cloudflare default transport). */
|
|
60
60
|
cloudflareSend?: CloudflareSend;
|
|
61
61
|
/**
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
* Pin the captured-mail inbox shard to a Cloudflare data-residency
|
|
63
|
+
* jurisdiction. Pass the same value as the worker's `jurisdiction` so the
|
|
64
|
+
* dev inbox co-resides with app data. Omit for the un-pinned global namespace.
|
|
65
|
+
*/
|
|
66
66
|
jurisdiction?: DurableObjectJurisdiction;
|
|
67
67
|
/** Shard the captured-mail inbox lives on; override if your worker sets a custom `defaultShardKey`. */
|
|
68
68
|
rootShard?: string;
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
-
* Whether outbound mail should be captured (into the studio inbox) rather than
|
|
72
|
-
* delivered. Explicit `LUNORA_MAIL_CAPTURE` (`"1"`/`"true"` vs `"0"`/`"false"`)
|
|
73
|
-
* always wins; unset, capture is on only in a development environment. It does
|
|
74
|
-
* NOT fall back to "no SEND_EMAIL binding ⇒ capture" — a production deploy that
|
|
75
|
-
* forgot the binding must fail loudly on send, not silently swallow mail.
|
|
76
|
-
*/
|
|
71
|
+
* Whether outbound mail should be captured (into the studio inbox) rather than
|
|
72
|
+
* delivered. Explicit `LUNORA_MAIL_CAPTURE` (`"1"`/`"true"` vs `"0"`/`"false"`)
|
|
73
|
+
* always wins; unset, capture is on only in a development environment. It does
|
|
74
|
+
* NOT fall back to "no SEND_EMAIL binding ⇒ capture" — a production deploy that
|
|
75
|
+
* forgot the binding must fail loudly on send, not silently swallow mail.
|
|
76
|
+
*/
|
|
77
77
|
declare const shouldCaptureMail: (env: MailEnv) => boolean;
|
|
78
78
|
/**
|
|
79
|
-
* Build the {@link MailboxSink} that records a captured message into the studio's
|
|
80
|
-
* root-shard inbox via the reserved `recordMail` admin RPC — the same
|
|
81
|
-
* worker→root-shard path the runtime uses for auth events. Best-effort: without
|
|
82
|
-
* the `SHARD` binding or `LUNORA_ADMIN_TOKEN` it returns a sentinel id so a send
|
|
83
|
-
* never fails for lack of somewhere to record.
|
|
84
|
-
*/
|
|
79
|
+
* Build the {@link MailboxSink} that records a captured message into the studio's
|
|
80
|
+
* root-shard inbox via the reserved `recordMail` admin RPC — the same
|
|
81
|
+
* worker→root-shard path the runtime uses for auth events. Best-effort: without
|
|
82
|
+
* the `SHARD` binding or `LUNORA_ADMIN_TOKEN` it returns a sentinel id so a send
|
|
83
|
+
* never fails for lack of somewhere to record.
|
|
84
|
+
*/
|
|
85
85
|
declare const createCaptureSink: (env: MailEnv, rootShard?: string, jurisdiction?: DurableObjectJurisdiction) => MailboxSink;
|
|
86
86
|
/**
|
|
87
|
-
* Build a {@link Mailer} from a Worker `env`. In a dev environment every send is
|
|
88
|
-
* captured into the studio's Mail inbox; otherwise it delivers via the supplied
|
|
89
|
-
* `cloudflareSend` (the `SEND_EMAIL` binding) or, failing that, `RESEND_API_KEY`.
|
|
90
|
-
* Throws when neither a capture context nor a real transport is available, so a
|
|
91
|
-
* misconfigured production deploy fails loudly instead of silently dropping mail.
|
|
92
|
-
*
|
|
93
|
-
* `MAIL_FROM` is required (the default sender).
|
|
94
|
-
*/
|
|
87
|
+
* Build a {@link Mailer} from a Worker `env`. In a dev environment every send is
|
|
88
|
+
* captured into the studio's Mail inbox; otherwise it delivers via the supplied
|
|
89
|
+
* `cloudflareSend` (the `SEND_EMAIL` binding) or, failing that, `RESEND_API_KEY`.
|
|
90
|
+
* Throws when neither a capture context nor a real transport is available, so a
|
|
91
|
+
* misconfigured production deploy fails loudly instead of silently dropping mail.
|
|
92
|
+
*
|
|
93
|
+
* `MAIL_FROM` is required (the default sender).
|
|
94
|
+
*/
|
|
95
95
|
declare const createMailerFromEnv: (env: MailEnv, options?: FromEnvOptions) => Mailer;
|
|
96
96
|
/** Serializable representation of a `SendOptions` payload — drops the `react` field. */
|
|
97
97
|
interface QueuedSend {
|
|
@@ -106,45 +106,45 @@ interface QueuedSend {
|
|
|
106
106
|
to: string | string[];
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
109
|
-
* Narrow a `SendOptions` to its serializable `QueuedSend` shape by dropping the
|
|
110
|
-
* non-cloneable `react` field. React elements are not structured-cloneable, so
|
|
111
|
-
* the queue body must carry only the pre-rendered html/text and scalar fields.
|
|
112
|
-
*/
|
|
109
|
+
* Narrow a `SendOptions` to its serializable `QueuedSend` shape by dropping the
|
|
110
|
+
* non-cloneable `react` field. React elements are not structured-cloneable, so
|
|
111
|
+
* the queue body must carry only the pre-rendered html/text and scalar fields.
|
|
112
|
+
*/
|
|
113
113
|
declare const toQueuedPayload: (options: SendOptions) => QueuedSend;
|
|
114
114
|
/**
|
|
115
|
-
* Helper used by Queue consumers: rehydrate a `QueuedSend` payload and forward
|
|
116
|
-
* to a configured `Mailer.send()`. Use this inside your Worker's `queue()`
|
|
117
|
-
* handler.
|
|
118
|
-
*
|
|
119
|
-
* ```ts
|
|
120
|
-
* export default {
|
|
121
|
-
* queue: async (batch, env) => {
|
|
122
|
-
* const mailer = createMailer({ apiKey: env.RESEND_API_KEY, from: "..." });
|
|
123
|
-
* for (const message of batch.messages) {
|
|
124
|
-
* await consumeQueuedSend(mailer, message.body);
|
|
125
|
-
* }
|
|
126
|
-
* },
|
|
127
|
-
* };
|
|
128
|
-
* ```
|
|
129
|
-
*/
|
|
115
|
+
* Helper used by Queue consumers: rehydrate a `QueuedSend` payload and forward
|
|
116
|
+
* to a configured `Mailer.send()`. Use this inside your Worker's `queue()`
|
|
117
|
+
* handler.
|
|
118
|
+
*
|
|
119
|
+
* ```ts
|
|
120
|
+
* export default {
|
|
121
|
+
* queue: async (batch, env) => {
|
|
122
|
+
* const mailer = createMailer({ apiKey: env.RESEND_API_KEY, from: "..." });
|
|
123
|
+
* for (const message of batch.messages) {
|
|
124
|
+
* await consumeQueuedSend(mailer, message.body);
|
|
125
|
+
* }
|
|
126
|
+
* },
|
|
127
|
+
* };
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
130
|
declare const consumeQueuedSend: (mailer: Mailer, payload: unknown) => Promise<{
|
|
131
131
|
id: string;
|
|
132
132
|
}>;
|
|
133
133
|
/**
|
|
134
|
-
* Render a React element to an HTML/text pair suitable for inlining into a
|
|
135
|
-
* provider payload. Wraps `@react-email/render` so we can swap to
|
|
136
|
-
* `@visulima/email`'s react-email template engine without touching callers.
|
|
137
|
-
*/
|
|
134
|
+
* Render a React element to an HTML/text pair suitable for inlining into a
|
|
135
|
+
* provider payload. Wraps `@react-email/render` so we can swap to
|
|
136
|
+
* `@visulima/email`'s react-email template engine without touching callers.
|
|
137
|
+
*/
|
|
138
138
|
declare const renderEmail: (element: ReactElement) => Promise<{
|
|
139
139
|
html: string;
|
|
140
140
|
text: string;
|
|
141
141
|
}>;
|
|
142
142
|
/**
|
|
143
|
-
* Build a Resend-backed transport via `@visulima/email`. Wraps the provider's
|
|
144
|
-
* `sendEmail()` into the minimal `{ send(payload) -> { id } }` shape the rest of
|
|
145
|
-
* `@lunora/mail` consumes. Kept reachable as a named export so a project that
|
|
146
|
-
* prefers Resend over the Cloudflare default can pass
|
|
147
|
-
* `transport: createResendTransport(apiKey, from)` to `createMailer`.
|
|
148
|
-
*/
|
|
143
|
+
* Build a Resend-backed transport via `@visulima/email`. Wraps the provider's
|
|
144
|
+
* `sendEmail()` into the minimal `{ send(payload) -> { id } }` shape the rest of
|
|
145
|
+
* `@lunora/mail` consumes. Kept reachable as a named export so a project that
|
|
146
|
+
* prefers Resend over the Cloudflare default can pass
|
|
147
|
+
* `transport: createResendTransport(apiKey, from)` to `createMailer`.
|
|
148
|
+
*/
|
|
149
149
|
declare const createResendTransport: (apiKey: string, defaultFrom: string) => MailTransport;
|
|
150
150
|
export { type CloudflareSend, type CloudflareTransportOptions, type FromEnvOptions, type LunoraMailOptions, type MailEnv, type MailTransport, type MailboxSink, type Mailer, type QueuedSend, type SendOptions, consumeQueuedSend, createCaptureSink, createCloudflareTransport, createMailer, createMailerFromEnv, createResendTransport, renderEmail, shouldCaptureMail, toQueuedPayload };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { createCloudflareTransport } from './packem_shared/createCloudflareTransport-yHOVEsZv.mjs';
|
|
3
|
-
export { default as createMailer } from './packem_shared/createMailer-oEKPAd4J.mjs';
|
|
4
|
-
export { createCaptureSink, createMailerFromEnv, shouldCaptureMail } from './packem_shared/createCaptureSink-DeihS4LH.mjs';
|
|
5
|
-
export { consumeQueuedSend, toQueuedPayload } from './packem_shared/consumeQueuedSend-BEKOdaxU.mjs';
|
|
6
|
-
export { default as renderEmail } from './packem_shared/renderEmail-hyS1bpVP.mjs';
|
|
7
|
-
export { default as createResendTransport } from './packem_shared/createResendTransport-oNIorpzv.mjs';
|
|
1
|
+
import{createCaptureTransport as a}from"./packem_shared/createCaptureTransport-CKc6NpZR.mjs";import{createCloudflareTransport as t}from"./packem_shared/createCloudflareTransport-E-UdW8Dc.mjs";import{default as u}from"./packem_shared/createMailer-LHgKs5yh.mjs";import{createCaptureSink as f,createMailerFromEnv as l,shouldCaptureMail as m}from"./packem_shared/createCaptureSink-CE24od5h.mjs";import{consumeQueuedSend as s,toQueuedPayload as c}from"./packem_shared/consumeQueuedSend-8TXPj5o-.mjs";import{default as i}from"./packem_shared/renderEmail-BOUnr6i3.mjs";import{default as M}from"./packem_shared/createResendTransport-CG9s8iI9.mjs";export{s as consumeQueuedSend,f as createCaptureSink,a as createCaptureTransport,t as createCloudflareTransport,u as createMailer,l as createMailerFromEnv,M as createResendTransport,i as renderEmail,m as shouldCaptureMail,c as toQueuedPayload};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{LunoraError as t}from"@lunora/errors";const n=320,a=256,d=/^([^<]*)<([^>]*)>\s*$/,i=(r,s)=>{if(s.includes("\r")||s.includes(`
|
|
2
|
+
`)||s.includes(","))throw new t("INTERNAL",`@lunora/mail: address ${r} must not contain CR, LF, or comma`)},f=(r,s)=>{if(s.includes("\r")||s.includes(`
|
|
3
|
+
`))throw new t("INTERNAL",`@lunora/mail: ${r} must not contain CR or LF`)},m=(r,s)=>{if(r.length>a)throw new t("INTERNAL",`@lunora/mail: address name must be <= ${String(a)} characters`);if(s.length>n)throw new t("INTERNAL",`@lunora/mail: address email must be <= ${String(n)} characters`);return r&&i("name",r),i("email",s),r?{email:s,name:r}:{email:s}},A=r=>{const s=r.trim();if(s.length>n)throw new t("INTERNAL",`@lunora/mail: address email must be <= ${String(n)} characters`);return i("email",s),{email:s}},c=r=>{const s=d.exec(r),e=(s?.[2]??"").trim();return s&&e?m((s[1]??"").trim(),e):A(r)},o=r=>r===void 0?void 0:(Array.isArray(r)?r:[r]).map(e=>c(e)),u=r=>{o(r.to),o(r.cc),o(r.bcc),r.from!==void 0&&c(r.from),r.replyTo!==void 0&&c(r.replyTo)};export{f as a,u as b,c,o as t};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Minimal projection of a Cloudflare Queue binding — accepts a JSON payload
|
|
4
|
+
* via `.send()`. Declared structurally so callers can pass either the real
|
|
5
|
+
* `Queue` binding or a unit-test double.
|
|
6
|
+
*/
|
|
7
|
+
interface QueueLike {
|
|
8
|
+
send: (payload: unknown, options?: Record<string, unknown>) => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Minimal projection of a transport adapter. Returning `{ id }` follows
|
|
12
|
+
* Resend's response shape; the real `@visulima/email` `MailManager` flattens
|
|
13
|
+
* provider responses to the same field for us.
|
|
14
|
+
*/
|
|
15
|
+
interface MailTransport {
|
|
16
|
+
send: (payload: SendPayload) => Promise<{
|
|
17
|
+
id: string;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
interface SendPayload {
|
|
21
|
+
bcc?: string[];
|
|
22
|
+
cc?: string[];
|
|
23
|
+
from?: string;
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
html?: string;
|
|
26
|
+
replyTo?: string;
|
|
27
|
+
subject: string;
|
|
28
|
+
text?: string;
|
|
29
|
+
to: string | string[];
|
|
30
|
+
}
|
|
31
|
+
interface SendOptions {
|
|
32
|
+
bcc?: string[];
|
|
33
|
+
cc?: string[];
|
|
34
|
+
from?: string;
|
|
35
|
+
headers?: Record<string, string>;
|
|
36
|
+
html?: string;
|
|
37
|
+
react?: ReactElement;
|
|
38
|
+
replyTo?: string;
|
|
39
|
+
subject: string;
|
|
40
|
+
text?: string;
|
|
41
|
+
to: string | string[];
|
|
42
|
+
}
|
|
43
|
+
interface LunoraMailOptions {
|
|
44
|
+
/** API key for the Resend transport (bring-your-own-provider). Ignored when `transport` or `cloudflareSend` is set. */
|
|
45
|
+
apiKey?: string;
|
|
46
|
+
/**
|
|
47
|
+
* RFC 822 send callback bound to the Worker's `send_email` binding. When set
|
|
48
|
+
* (and no explicit `transport` is supplied) the default transport is
|
|
49
|
+
* Cloudflare Email Workers — Lunora's default provider. Ignored when
|
|
50
|
+
* `transport` is set.
|
|
51
|
+
*/
|
|
52
|
+
cloudflareSend?: (from: string, to: string, raw: string) => Promise<void>;
|
|
53
|
+
/** Default sender (`Name <addr@host>` or bare email). */
|
|
54
|
+
from: string;
|
|
55
|
+
/** Default queue binding for `mailer.queue()`. */
|
|
56
|
+
queue?: QueueLike;
|
|
57
|
+
/** Override the underlying transport. Useful for tests, the dev capture transport, + multi-provider setups. */
|
|
58
|
+
transport?: MailTransport;
|
|
59
|
+
}
|
|
60
|
+
interface Mailer {
|
|
61
|
+
queue: (options: SendOptions) => Promise<{
|
|
62
|
+
queued: true;
|
|
63
|
+
}>;
|
|
64
|
+
send: (options: SendOptions) => Promise<{
|
|
65
|
+
id: string;
|
|
66
|
+
}>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* One captured outbound message as persisted by the dev mail catcher. Extends
|
|
70
|
+
* the rendered, validated {@link SendPayload} with an `id` and a capture
|
|
71
|
+
* timestamp assigned by the sink (the root-shard mailbox), so the studio inbox
|
|
72
|
+
* can list and open it.
|
|
73
|
+
*
|
|
74
|
+
* **Canonical captured-mail wire type — single source of truth.** Every other
|
|
75
|
+
* representation of a captured message mirrors this shape; consumers import it
|
|
76
|
+
* directly wherever the package dependency direction allows.
|
|
77
|
+
*
|
|
78
|
+
* `@lunora/studio`'s `CapturedMail` re-exports this (type-only dep on
|
|
79
|
+
* `@lunora/mail`). `@lunora/do`'s `CapturedMailRow` / `RecordMailInput` are
|
|
80
|
+
* documented mirrors — the DO runtime stays free of any `@lunora/mail` *runtime*
|
|
81
|
+
* dep — guarded by a compile-time structural assertion against this type, so a
|
|
82
|
+
* field added here that isn't mirrored fails the `@lunora/do` build.
|
|
83
|
+
*
|
|
84
|
+
* Add or change a captured-mail field here first; the guards will point at the
|
|
85
|
+
* mirrors that need the matching change.
|
|
86
|
+
*/
|
|
87
|
+
interface CapturedMail extends SendPayload {
|
|
88
|
+
/** Epoch-ms the message was captured. */
|
|
89
|
+
capturedAt: number;
|
|
90
|
+
/** Stable id assigned to the captured message. */
|
|
91
|
+
id: string;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Minimal projection of the persistence target the capture transport writes to
|
|
95
|
+
* (the studio's root-shard mailbox). Declared structurally — like
|
|
96
|
+
* {@link import("./types").QueueLike} — so `@lunora/mail` stays free of any
|
|
97
|
+
* Durable Object / runtime dependency. The registry scaffold supplies the
|
|
98
|
+
* concrete sink that POSTs to the root shard's `__lunora_admin__:recordMail` RPC.
|
|
99
|
+
*/
|
|
100
|
+
interface MailboxSink {
|
|
101
|
+
record: (mail: SendPayload) => Promise<{
|
|
102
|
+
id: string;
|
|
103
|
+
}>;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Build a capture {@link MailTransport}: instead of delivering, it persists the
|
|
107
|
+
* fully rendered + validated payload to `sink` and returns the assigned id.
|
|
108
|
+
*
|
|
109
|
+
* Wired in dev by the mail registry scaffold so `lunora dev` shows every send in
|
|
110
|
+
* the studio's Mail inbox — including `@lunora/auth`'s verification and
|
|
111
|
+
* forgot-password mail — with no provider credentials and nothing leaving the
|
|
112
|
+
* machine. Address/header validation already ran in `createMailer.buildPayload`
|
|
113
|
+
* before the payload reaches here, so the captured message is the same one a
|
|
114
|
+
* real transport would have sent.
|
|
115
|
+
*/
|
|
116
|
+
declare const createCaptureTransport: (sink: MailboxSink) => MailTransport;
|
|
117
|
+
export { CapturedMail as C, LunoraMailOptions as L, MailTransport as M, QueueLike as Q, SendOptions as S, Mailer as a, MailboxSink as b, SendPayload as c, createCaptureTransport as d };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Minimal projection of a Cloudflare Queue binding — accepts a JSON payload
|
|
4
|
+
* via `.send()`. Declared structurally so callers can pass either the real
|
|
5
|
+
* `Queue` binding or a unit-test double.
|
|
6
|
+
*/
|
|
7
|
+
interface QueueLike {
|
|
8
|
+
send: (payload: unknown, options?: Record<string, unknown>) => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Minimal projection of a transport adapter. Returning `{ id }` follows
|
|
12
|
+
* Resend's response shape; the real `@visulima/email` `MailManager` flattens
|
|
13
|
+
* provider responses to the same field for us.
|
|
14
|
+
*/
|
|
15
|
+
interface MailTransport {
|
|
16
|
+
send: (payload: SendPayload) => Promise<{
|
|
17
|
+
id: string;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
interface SendPayload {
|
|
21
|
+
bcc?: string[];
|
|
22
|
+
cc?: string[];
|
|
23
|
+
from?: string;
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
html?: string;
|
|
26
|
+
replyTo?: string;
|
|
27
|
+
subject: string;
|
|
28
|
+
text?: string;
|
|
29
|
+
to: string | string[];
|
|
30
|
+
}
|
|
31
|
+
interface SendOptions {
|
|
32
|
+
bcc?: string[];
|
|
33
|
+
cc?: string[];
|
|
34
|
+
from?: string;
|
|
35
|
+
headers?: Record<string, string>;
|
|
36
|
+
html?: string;
|
|
37
|
+
react?: ReactElement;
|
|
38
|
+
replyTo?: string;
|
|
39
|
+
subject: string;
|
|
40
|
+
text?: string;
|
|
41
|
+
to: string | string[];
|
|
42
|
+
}
|
|
43
|
+
interface LunoraMailOptions {
|
|
44
|
+
/** API key for the Resend transport (bring-your-own-provider). Ignored when `transport` or `cloudflareSend` is set. */
|
|
45
|
+
apiKey?: string;
|
|
46
|
+
/**
|
|
47
|
+
* RFC 822 send callback bound to the Worker's `send_email` binding. When set
|
|
48
|
+
* (and no explicit `transport` is supplied) the default transport is
|
|
49
|
+
* Cloudflare Email Workers — Lunora's default provider. Ignored when
|
|
50
|
+
* `transport` is set.
|
|
51
|
+
*/
|
|
52
|
+
cloudflareSend?: (from: string, to: string, raw: string) => Promise<void>;
|
|
53
|
+
/** Default sender (`Name <addr@host>` or bare email). */
|
|
54
|
+
from: string;
|
|
55
|
+
/** Default queue binding for `mailer.queue()`. */
|
|
56
|
+
queue?: QueueLike;
|
|
57
|
+
/** Override the underlying transport. Useful for tests, the dev capture transport, + multi-provider setups. */
|
|
58
|
+
transport?: MailTransport;
|
|
59
|
+
}
|
|
60
|
+
interface Mailer {
|
|
61
|
+
queue: (options: SendOptions) => Promise<{
|
|
62
|
+
queued: true;
|
|
63
|
+
}>;
|
|
64
|
+
send: (options: SendOptions) => Promise<{
|
|
65
|
+
id: string;
|
|
66
|
+
}>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* One captured outbound message as persisted by the dev mail catcher. Extends
|
|
70
|
+
* the rendered, validated {@link SendPayload} with an `id` and a capture
|
|
71
|
+
* timestamp assigned by the sink (the root-shard mailbox), so the studio inbox
|
|
72
|
+
* can list and open it.
|
|
73
|
+
*
|
|
74
|
+
* **Canonical captured-mail wire type — single source of truth.** Every other
|
|
75
|
+
* representation of a captured message mirrors this shape; consumers import it
|
|
76
|
+
* directly wherever the package dependency direction allows.
|
|
77
|
+
*
|
|
78
|
+
* `@lunora/studio`'s `CapturedMail` re-exports this (type-only dep on
|
|
79
|
+
* `@lunora/mail`). `@lunora/do`'s `CapturedMailRow` / `RecordMailInput` are
|
|
80
|
+
* documented mirrors — the DO runtime stays free of any `@lunora/mail` *runtime*
|
|
81
|
+
* dep — guarded by a compile-time structural assertion against this type, so a
|
|
82
|
+
* field added here that isn't mirrored fails the `@lunora/do` build.
|
|
83
|
+
*
|
|
84
|
+
* Add or change a captured-mail field here first; the guards will point at the
|
|
85
|
+
* mirrors that need the matching change.
|
|
86
|
+
*/
|
|
87
|
+
interface CapturedMail extends SendPayload {
|
|
88
|
+
/** Epoch-ms the message was captured. */
|
|
89
|
+
capturedAt: number;
|
|
90
|
+
/** Stable id assigned to the captured message. */
|
|
91
|
+
id: string;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Minimal projection of the persistence target the capture transport writes to
|
|
95
|
+
* (the studio's root-shard mailbox). Declared structurally — like
|
|
96
|
+
* {@link import("./types").QueueLike} — so `@lunora/mail` stays free of any
|
|
97
|
+
* Durable Object / runtime dependency. The registry scaffold supplies the
|
|
98
|
+
* concrete sink that POSTs to the root shard's `__lunora_admin__:recordMail` RPC.
|
|
99
|
+
*/
|
|
100
|
+
interface MailboxSink {
|
|
101
|
+
record: (mail: SendPayload) => Promise<{
|
|
102
|
+
id: string;
|
|
103
|
+
}>;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Build a capture {@link MailTransport}: instead of delivering, it persists the
|
|
107
|
+
* fully rendered + validated payload to `sink` and returns the assigned id.
|
|
108
|
+
*
|
|
109
|
+
* Wired in dev by the mail registry scaffold so `lunora dev` shows every send in
|
|
110
|
+
* the studio's Mail inbox — including `@lunora/auth`'s verification and
|
|
111
|
+
* forgot-password mail — with no provider credentials and nothing leaving the
|
|
112
|
+
* machine. Address/header validation already ran in `createMailer.buildPayload`
|
|
113
|
+
* before the payload reaches here, so the captured message is the same one a
|
|
114
|
+
* real transport would have sent.
|
|
115
|
+
*/
|
|
116
|
+
declare const createCaptureTransport: (sink: MailboxSink) => MailTransport;
|
|
117
|
+
export { CapturedMail as C, LunoraMailOptions as L, MailTransport as M, QueueLike as Q, SendOptions as S, Mailer as a, MailboxSink as b, SendPayload as c, createCaptureTransport as d };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as c}from"@lunora/errors";const d=n=>{const t={...n};return delete t.react,t},p=async(n,t)=>{if(!t||typeof t!="object"||Array.isArray(t))throw new c("INTERNAL","@lunora/mail: queue message body must be an object");const e=t;if(typeof e.subject!="string")throw new TypeError("@lunora/mail: queue message must have a string `subject`");const m=typeof e.to=="string",y=Array.isArray(e.to)&&e.to.every(s=>typeof s=="string");if(!m&&!y)throw new c("INTERNAL","@lunora/mail: queue message `to` must be a string or string[]");const o=(s,r)=>{if(r!==void 0){if(typeof r!="string")throw new TypeError(`@lunora/mail: queue message \`${s}\` must be a string`);return r}},a=(s,r)=>{if(r!==void 0){if(typeof r=="string")return[r];if(Array.isArray(r)&&r.every(i=>typeof i=="string"))return r;throw new TypeError(`@lunora/mail: queue message \`${s}\` must be a string or string[]`)}};let u;if(e.headers!==void 0){if(!e.headers||typeof e.headers!="object"||Array.isArray(e.headers))throw new TypeError("@lunora/mail: queue message `headers` must be an object of string values");const s=Object.entries(e.headers);for(const[r,i]of s)if(typeof i!="string")throw new TypeError(`@lunora/mail: queue message header "${r}" must be a string`);u=e.headers}const f={bcc:a("bcc",e.bcc),cc:a("cc",e.cc),from:o("from",e.from),headers:u,html:o("html",e.html),replyTo:o("replyTo",e.replyTo),subject:e.subject,text:o("text",e.text),to:e.to};return n.send(f)};export{p as consumeQueuedSend,d as toQueuedPayload};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createCaptureTransport as d}from"./createCaptureTransport-CKc6NpZR.mjs";import"./createCloudflareTransport-E-UdW8Dc.mjs";import n from"./createMailer-LHgKs5yh.mjs";import{LunoraError as c}from"@lunora/errors";import{D as s,p as l}from"./shard-C0aRRoVy.mjs";import"./consumeQueuedSend-8TXPj5o-.mjs";import"./renderEmail-BOUnr6i3.mjs";import"./createResendTransport-CG9s8iI9.mjs";const E="__lunora_admin__:recordMail",p=/^(?:dev(?:elopment)?|local(?:host)?|test)$/iu,f=["CF_ENV","ENVIRONMENT","NODE_ENV","WORKER_ENV"],_=(t,e)=>{const r=t[e];if(typeof r!="string"||r==="")throw new c("INTERNAL",`@lunora/mail: missing env var \`${e}\` — set it in .dev.vars (and \`wrangler secret put ${e}\` for secrets).`);return r},N=t=>{const e=t.LUNORA_MAIL_CAPTURE;if(typeof e=="string"){const r=e.toLowerCase();if(r==="1"||r==="true")return!0;if(r==="0"||r==="false")return!1;console.warn(`@lunora/mail: unrecognized LUNORA_MAIL_CAPTURE value "${e}" — expected "1"/"true" or "0"/"false"; falling back to environment detection.`)}return f.some(r=>{const o=t[r];return typeof o=="string"&&p.test(o)})},m=(t,e=s,r)=>({record:async o=>{const i=t.SHARD,a=typeof t.LUNORA_ADMIN_TOKEN=="string"?t.LUNORA_ADMIN_TOKEN:void 0;if(i===void 0||a===void 0)return{id:"uncaptured"};try{return{id:(await l(i,{adminToken:a,envelope:{args:o,functionPath:E},jurisdiction:r,label:"@lunora/mail: recording captured mail",shardKey:e})).result?.id??"captured"}}catch(u){return console.error("@lunora/mail: failed to record captured mail into the studio inbox —",u),{id:"uncaptured"}}}}),L=(t,e={})=>{const r=_(t,"MAIL_FROM");if(N(t))return n({from:r,transport:d(m(t,e.rootShard,e.jurisdiction))});if(e.cloudflareSend)return n({cloudflareSend:e.cloudflareSend,from:r});const o=typeof t.RESEND_API_KEY=="string"?t.RESEND_API_KEY:void 0;if(o!==void 0&&o!=="")return n({apiKey:o,from:r});throw new c("INTERNAL","@lunora/mail: no transport configured — provide `cloudflareSend` (a SEND_EMAIL binding) or RESEND_API_KEY, or run in a dev environment to capture.")};export{m as createCaptureSink,L as createMailerFromEnv,N as shouldCaptureMail};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const t=Symbol.for("@lunora/mail.captureTransport"),a=r=>r[t]===!0,s=r=>({[t]:!0,send:async o=>r.record(o)});export{t as CAPTURE_TRANSPORT_BRAND,s as createCaptureTransport,a as isCaptureTransport};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as n}from"@lunora/errors";import{cloudflareEmailProvider as l}from"@visulima/email/providers/cloudflare-email";import{r as d,t as u,i as m}from"./provider-transport-DXmTG_xv.mjs";const E=r=>{const t=l({send:r.send});return{send:async e=>{await t.initialize();const o=e.cc!==void 0&&e.cc.length>0,s=e.bcc!==void 0&&e.bcc.length>0;if(o||s)throw new n("INTERNAL","@lunora/mail: Cloudflare Email Workers does not support cc/bcc — fan out one send per recipient instead");const{first:c,list:i}=d(e.to);if(i.length>1)throw new n("INTERNAL",`@lunora/mail: Cloudflare Email Workers is single-recipient but received ${String(i.length)} \`to\` addresses — fan out one send per recipient instead`);const a=await t.sendEmail(u(e,r.from,c));return m(a)}}};export{E as createCloudflareTransport};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as c}from"@lunora/errors";import{D as d,p as u}from"./shard-C0aRRoVy.mjs";const f="message could not be processed",h=(r,e)=>{console.error("@lunora/mail/inbound: dropping message —",r),e.message.setReject(f)},g=r=>{const e=r.onError??h;return async(n,t,s)=>{const a={ctx:s,env:t,message:n};try{const o=await r.parse(n.raw);if(r.verify&&await r.verify(o,a)===!1)throw new c("INTERNAL","@lunora/mail/inbound: sender verification rejected the message");await r.dispatch(o,a)}catch(o){await e(o,a)}}},i=32768,l=r=>{let e="";for(let n=0;n<r.length;n+=i)e+=String.fromCharCode(...r.subarray(n,n+i));return btoa(e)},E=r=>r.attachments.length===0?r:{...r,attachments:r.attachments.map(e=>{const{content:n}=e;if(typeof n=="string")return e;const t=n instanceof Uint8Array?n:new Uint8Array(n);return{...e,content:l(t),encoding:"base64"}})},y=r=>{const e=r.shardKey??d,n=r.resolveArgs??(t=>E(t));return async(t,s)=>{const a=r.adminToken??(typeof s.env.LUNORA_ADMIN_TOKEN=="string"?s.env.LUNORA_ADMIN_TOKEN:void 0);if(a===void 0||a==="")throw new c("INTERNAL","@lunora/mail/inbound: missing LUNORA_ADMIN_TOKEN — cannot authorize inbound dispatch to the shard RPC.");const o={args:n(t,s),functionPath:r.functionPath,shardKey:e};await u(r.shard,{adminToken:a,envelope:o,jurisdiction:r.jurisdiction,label:`@lunora/mail/inbound: dispatch to \`${r.functionPath}\``,shardKey:e})}};export{g as createInboundEmailHandler,y as dispatchToLunoraFunction};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as f}from"@lunora/errors";import{a as l,b as n}from"./address-BQ5RUGdT.mjs";import{isCaptureTransport as o}from"./createCaptureTransport-CKc6NpZR.mjs";import{createCloudflareTransport as i}from"./createCloudflareTransport-E-UdW8Dc.mjs";import{toQueuedPayload as y}from"./consumeQueuedSend-8TXPj5o-.mjs";import b from"./renderEmail-BOUnr6i3.mjs";import h from"./createResendTransport-CG9s8iI9.mjs";const w=r=>{if(r.cloudflareSend)return i({from:r.from,send:r.cloudflareSend});if(r.apiKey)return h(r.apiKey,r.from);throw new f("INTERNAL","@lunora/mail: a transport is required — pass `transport`, `cloudflareSend` (Cloudflare Email Workers, the default), or `apiKey` (Resend)")},x=r=>{if(!r.from)throw new f("INTERNAL","@lunora/mail: `from` is required");const c=r.transport??w(r),d=async e=>{let{html:a}=e,{text:u}=e;if(e.react){const t=await b(e.react);a=a??t.html,u=u??t.text}if(l("subject",e.subject),e.headers)for(const[t,s]of Object.entries(e.headers))l(`header name "${t}"`,t),l(`header "${t}" value`,s);const m=e.from??r.from;return n({bcc:e.bcc,cc:e.cc,from:m,replyTo:e.replyTo,to:e.to}),{bcc:e.bcc,cc:e.cc,from:m,headers:e.headers,html:a,replyTo:e.replyTo,subject:e.subject,text:u,to:e.to}};return{queue:async e=>{if(!r.queue){if(o(c)){const u=await d(e);return await c.send(u),{queued:!0}}throw new f("INTERNAL","@lunora/mail: `queue` binding is required for mailer.queue()")}const a=await d(e);return await r.queue.send(y(a)),{queued:!0}},send:async e=>{const a=await d(e);return c.send(a)}}};export{x as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{resendProvider as a}from"@visulima/email/providers/resend";import{r as d,t as c,i as l}from"./provider-transport-DXmTG_xv.mjs";const u=(i,s)=>{const e=a({apiKey:i});return{send:async r=>{await e.initialize();const{first:n,list:t}=d(r.to),o=await e.sendEmail(c(r,s,t.length===1?n:t));return l(o)}}};export{u as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import u from"postal-mime";import{a as r}from"./address-BQ5RUGdT.mjs";const s=(o,e)=>{if(e!==void 0)return r(`inbound ${o}`,e),e},m=o=>o.address!==void 0&&o.address!==""?o.name?`${o.name} <${o.address}>`:o.address:o.group?o.group.map(e=>e.address??"").filter(e=>e!=="").join(", "):o.name??"",t=(o,e)=>new RegExp(String.raw`\b${e}=([a-zA-Z]+)`,"i").exec(o)?.[1]?.toLowerCase()??null,p=o=>o===void 0||o===""?{dkim:null,dmarc:null,spf:null}:{dkim:t(o,"dkim"),dmarc:t(o,"dmarc"),spf:t(o,"spf")},b=async o=>{const e=await u.parse(o),i={};for(const n of e.headers)r(`inbound header \`${n.key}\``,n.value),i[n.key]=n.value;const c=e.headers.find(n=>n.key==="authentication-results")?.value,f=(e.to??[]).map(n=>{const a=m(n);return r("inbound to",a),a}),d=e.from?m(e.from):"";return r("inbound from",d),{attachments:e.attachments.map(n=>({content:n.content,disposition:n.disposition,...n.encoding===void 0?{}:{encoding:n.encoding},filename:n.filename,mimeType:n.mimeType})),authentication:p(c),from:d,headers:i,...e.html===void 0?{}:{html:e.html},...s("inReplyTo",e.inReplyTo)===void 0?{}:{inReplyTo:e.inReplyTo},...s("messageId",e.messageId)===void 0?{}:{messageId:e.messageId},...s("references",e.references)===void 0?{}:{references:e.references},...s("subject",e.subject)===void 0?{}:{subject:e.subject},...e.text===void 0?{}:{text:e.text},to:f}};export{b as parseInboundEmail};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as o}from"@lunora/errors";import{t as n,c as s}from"./address-BQ5RUGdT.mjs";const c=e=>e instanceof Error?e.message:e==null?"send failed":typeof e=="string"?e:typeof e=="number"||typeof e=="boolean"||typeof e=="bigint"?e.toString():JSON.stringify(e)??"send failed",u=e=>{const t=n(e),[i]=t??[];if(!t||i===void 0)throw new o("INTERNAL","@lunora/mail: at least one recipient is required");return{first:i,list:t}},d=(e,t,i)=>({bcc:n(e.bcc),cc:n(e.cc),from:s(e.from??t),headers:e.headers,html:e.html,replyTo:e.replyTo?s(e.replyTo):void 0,subject:e.subject,text:e.text,to:i}),m=e=>{if(!e.success||!e.data)throw console.error(`@lunora/mail: send failed: ${c(e.error)}`),new o("INTERNAL","@lunora/mail: send failed");return{id:e.data.messageId}};export{m as i,u as r,d as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{render as r}from"@react-email/render";const n=async t=>{const[e,a]=await Promise.all([r(t,{pretty:!1}),r(t,{plainText:!0})]);return{html:e,text:a}};export{n as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as a}from"@lunora/errors";const u="__root__",s=(t,r)=>{if(r===void 0)return t;if(typeof t.jurisdiction!="function")throw new TypeError(`@lunora/mail: Durable Object namespace does not support jurisdiction("${r}") — update @cloudflare/workers-types or remove the jurisdiction option`);return t.jurisdiction(r)},l=async(t,r)=>{const i=s(t,r.jurisdiction),e=await i.get(i.idFromName(r.shardKey)).fetch("https://shard.internal/rpc",{body:JSON.stringify(r.envelope),headers:{authorization:`Bearer ${r.adminToken}`,"content-type":"application/json"},method:"POST"});if(e.ok===!1)throw new a("INTERNAL",`${r.label} failed (HTTP ${String(e.status??"?")}).`);const o=await e.json();if(typeof o=="object"&&o!==null&&"error"in o){const{error:n}=o;if(n!=null)throw new a("INTERNAL",`${r.label} returned an error: ${JSON.stringify(n)}`)}return o};export{u as D,l as p};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Structural projection of one shard stub — only `fetch` returning a Fetch-`Response`-like object. */
|
|
2
|
+
interface ShardStubLike {
|
|
3
|
+
fetch: (input: string, init?: {
|
|
4
|
+
body?: string;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
method?: string;
|
|
7
|
+
}) => Promise<{
|
|
8
|
+
json: () => Promise<unknown>;
|
|
9
|
+
ok?: boolean;
|
|
10
|
+
status?: number;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Cloudflare Durable Object data-residency jurisdiction. Widening union —
|
|
15
|
+
* Cloudflare adds values over time.
|
|
16
|
+
* @see https://developers.cloudflare.com/durable-objects/reference/data-location/
|
|
17
|
+
*/
|
|
18
|
+
type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
|
|
19
|
+
/** Structural projection of the `SHARD` Durable Object namespace. */
|
|
20
|
+
interface ShardNamespaceLike {
|
|
21
|
+
get: (id: unknown) => ShardStubLike;
|
|
22
|
+
idFromName: (name: string) => unknown;
|
|
23
|
+
/**
|
|
24
|
+
* Derive a jurisdiction-restricted subnamespace. Optional because older
|
|
25
|
+
* workers-types releases (and test doubles) may not expose it.
|
|
26
|
+
*/
|
|
27
|
+
jurisdiction?: (jurisdiction: DurableObjectJurisdiction) => ShardNamespaceLike;
|
|
28
|
+
}
|
|
29
|
+
export { DurableObjectJurisdiction as D, ShardNamespaceLike as S, ShardStubLike as a };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Structural projection of one shard stub — only `fetch` returning a Fetch-`Response`-like object. */
|
|
2
|
+
interface ShardStubLike {
|
|
3
|
+
fetch: (input: string, init?: {
|
|
4
|
+
body?: string;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
method?: string;
|
|
7
|
+
}) => Promise<{
|
|
8
|
+
json: () => Promise<unknown>;
|
|
9
|
+
ok?: boolean;
|
|
10
|
+
status?: number;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Cloudflare Durable Object data-residency jurisdiction. Widening union —
|
|
15
|
+
* Cloudflare adds values over time.
|
|
16
|
+
* @see https://developers.cloudflare.com/durable-objects/reference/data-location/
|
|
17
|
+
*/
|
|
18
|
+
type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
|
|
19
|
+
/** Structural projection of the `SHARD` Durable Object namespace. */
|
|
20
|
+
interface ShardNamespaceLike {
|
|
21
|
+
get: (id: unknown) => ShardStubLike;
|
|
22
|
+
idFromName: (name: string) => unknown;
|
|
23
|
+
/**
|
|
24
|
+
* Derive a jurisdiction-restricted subnamespace. Optional because older
|
|
25
|
+
* workers-types releases (and test doubles) may not expose it.
|
|
26
|
+
*/
|
|
27
|
+
jurisdiction?: (jurisdiction: DurableObjectJurisdiction) => ShardNamespaceLike;
|
|
28
|
+
}
|
|
29
|
+
export { DurableObjectJurisdiction as D, ShardNamespaceLike as S, ShardStubLike as a };
|
package/dist/testing.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as CapturedMail } from "./packem_shared/capture-transport.d-
|
|
1
|
+
import { C as CapturedMail } from "./packem_shared/capture-transport.d-_rIl9W6N.mjs";
|
|
2
2
|
import 'react';
|
|
3
3
|
/** Minimal `fetch` projection so a test can inject a stub. */
|
|
4
4
|
type FetchLike = (input: string, init?: {
|
|
@@ -33,16 +33,16 @@ interface WaitForMailOptions extends InboxOptions {
|
|
|
33
33
|
/** Read the captured-mail inbox (newest first). */
|
|
34
34
|
declare const listCapturedMail: (options: InboxOptions) => Promise<CapturedMail[]>;
|
|
35
35
|
/**
|
|
36
|
-
* Poll the captured-mail inbox until a message addressed to `to` (optionally
|
|
37
|
-
* matching `subjectMatch`) appears, then return it. Throws on timeout. Entries
|
|
38
|
-
* are newest-first, so the most recent matching message wins.
|
|
39
|
-
*/
|
|
36
|
+
* Poll the captured-mail inbox until a message addressed to `to` (optionally
|
|
37
|
+
* matching `subjectMatch`) appears, then return it. Throws on timeout. Entries
|
|
38
|
+
* are newest-first, so the most recent matching message wins.
|
|
39
|
+
*/
|
|
40
40
|
declare const waitForMail: (options: WaitForMailOptions) => Promise<CapturedMail>;
|
|
41
41
|
/**
|
|
42
|
-
* Pull the first link out of a captured message — html first, then text. Pass
|
|
43
|
-
* `match` to require the URL contain a substring (e.g. `"/reset-password"`),
|
|
44
|
-
* which disambiguates the action link from a logo/footer URL.
|
|
45
|
-
*/
|
|
42
|
+
* Pull the first link out of a captured message — html first, then text. Pass
|
|
43
|
+
* `match` to require the URL contain a substring (e.g. `"/reset-password"`),
|
|
44
|
+
* which disambiguates the action link from a logo/footer URL.
|
|
45
|
+
*/
|
|
46
46
|
declare const extractLink: (mail: CapturedMail, options?: {
|
|
47
47
|
match?: string;
|
|
48
48
|
}) => string;
|