@lunora/mail 1.0.0-alpha.60 → 1.0.0-alpha.62
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/dist/inbound/index.d.mts +73 -25
- package/dist/inbound/index.d.ts +73 -25
- package/dist/inbound/index.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/authenticatesFrom-xpKAFMRN.mjs +1 -0
- package/dist/packem_shared/{createCaptureSink-4T2LJOYC.mjs → createCaptureSink-wbDNGuA-.mjs} +1 -1
- package/dist/packem_shared/{createInboundEmailHandler-C6V47mz5.mjs → createInboundEmailHandler-vXSNL5mZ.mjs} +1 -1
- package/dist/packem_shared/createMailer-CjQmT7Ln.mjs +1 -0
- package/dist/packem_shared/shard-CRhAKK93.mjs +1 -0
- package/package.json +2 -2
- package/dist/packem_shared/createMailer-BJs2hnV-.mjs +0 -1
- package/dist/packem_shared/parseInboundEmail-BG0NU_e9.mjs +0 -1
- package/dist/packem_shared/shard-C0aRRoVy.mjs +0 -1
package/dist/inbound/index.d.mts
CHANGED
|
@@ -15,6 +15,18 @@ interface InboundAttachment {
|
|
|
15
15
|
/** Declared MIME type (e.g. `image/png`). */
|
|
16
16
|
mimeType: string;
|
|
17
17
|
}
|
|
18
|
+
/** One reported `method=result [ptype.property=value]` clause of an `Authentication-Results` header. */
|
|
19
|
+
interface InboundAuthResult {
|
|
20
|
+
/**
|
|
21
|
+
* The identifier the result is about, lowercased — DKIM's signing domain
|
|
22
|
+
* (`header.d=`), SPF's envelope `MAIL FROM` domain (`smtp.mailfrom=`, local
|
|
23
|
+
* part dropped) or DMARC's `header.from=`. `null` when the clause reported
|
|
24
|
+
* none, in which case it cannot be aligned and vouches for nothing.
|
|
25
|
+
*/
|
|
26
|
+
domain: string | null;
|
|
27
|
+
/** The result token, lowercased (`"pass"`, `"fail"`, `"none"`, …). */
|
|
28
|
+
result: string;
|
|
29
|
+
}
|
|
18
30
|
/**
|
|
19
31
|
* Sender-authentication verdicts pulled from the `Authentication-Results` header
|
|
20
32
|
* the receiving MX (e.g. Cloudflare Email Routing) stamped on the message.
|
|
@@ -24,17 +36,27 @@ interface InboundAttachment {
|
|
|
24
36
|
* spoofable, so a downstream handler MUST NOT make trust/authorization decisions
|
|
25
37
|
* on `email.from` alone — gate on these verdicts (or your own policy) instead.
|
|
26
38
|
* Verdicts are best-effort: when the receiving MX did not stamp an
|
|
27
|
-
* `Authentication-Results` header, every
|
|
39
|
+
* `Authentication-Results` header, every list is empty ("unknown").
|
|
40
|
+
*
|
|
41
|
+
* Each method holds a LIST, because RFC 8601 lets one header report the same
|
|
42
|
+
* method more than once and real mail does. An ESP-relayed message carries two
|
|
43
|
+
* DKIM signatures — the relay's and the author domain's — and the MX stamps a
|
|
44
|
+
* clause per signature in whatever order it verified them. Keeping only the
|
|
45
|
+
* first threw the aligned one away whenever it was not the one that happened to
|
|
46
|
+
* come first, and the message was rejected as unauthenticated. A consumer
|
|
47
|
+
* therefore asks "does ANY reported clause pass and align?", not "did the first
|
|
48
|
+
* one?".
|
|
28
49
|
*
|
|
29
50
|
* SECURITY: a bare `"pass"` is NOT proof the `From` header is genuine. SPF
|
|
30
51
|
* authenticates the envelope `MAIL FROM` domain and DKIM the signing domain
|
|
31
52
|
* (`d=`), and an attacker controls both — `spf=pass smtp.mailfrom=evil.example;
|
|
32
53
|
* dkim=pass header.d=evil.example` is routine for a message whose `From` says
|
|
33
54
|
* `ceo@victim.example`. Each verdict therefore carries the identifier it is
|
|
34
|
-
* about (
|
|
35
|
-
* domain equals the `From` address's domain (RFC 7489 strict
|
|
36
|
-
* DMARC pass already checked alignment for you. A pass with
|
|
37
|
-
* reported cannot be aligned and must be treated as
|
|
55
|
+
* about ({@link InboundAuthResult.domain}): an SPF or DKIM pass only vouches for
|
|
56
|
+
* `from` when that domain equals the `From` address's domain (RFC 7489 strict
|
|
57
|
+
* alignment). Only a DMARC pass already checked alignment for you. A pass with
|
|
58
|
+
* no identifier reported cannot be aligned and must be treated as
|
|
59
|
+
* unauthenticated.
|
|
38
60
|
*
|
|
39
61
|
* SECURITY: verdicts are read from the **first/topmost** `Authentication-Results`
|
|
40
62
|
* header in document order. The receiving MX prepends its own genuine header per
|
|
@@ -45,18 +67,12 @@ interface InboundAttachment {
|
|
|
45
67
|
* config this runtime-agnostic parser does not carry, so it is left to the host.
|
|
46
68
|
*/
|
|
47
69
|
interface InboundAuthentication {
|
|
48
|
-
/** DKIM
|
|
49
|
-
dkim:
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
/** `From` domain the DMARC verdict evaluated (`header.from=`, lowercased), or `null` when not reported. */
|
|
55
|
-
dmarcDomain: string | null;
|
|
56
|
-
/** SPF verdict (`"pass"`/`"fail"`/…), or `null` when not reported. */
|
|
57
|
-
spf: string | null;
|
|
58
|
-
/** Envelope `MAIL FROM` domain the SPF verdict is about (`smtp.mailfrom=`, local part dropped, lowercased), or `null` when not reported. */
|
|
59
|
-
spfDomain: string | null;
|
|
70
|
+
/** Every DKIM clause the header reported, in header order; empty when the method was not reported. */
|
|
71
|
+
dkim: InboundAuthResult[];
|
|
72
|
+
/** Every DMARC clause the header reported, in header order; empty when the method was not reported. */
|
|
73
|
+
dmarc: InboundAuthResult[];
|
|
74
|
+
/** Every SPF clause the header reported, in header order; empty when the method was not reported. */
|
|
75
|
+
spf: InboundAuthResult[];
|
|
60
76
|
}
|
|
61
77
|
/** Normalised, transport-agnostic view of a received message. */
|
|
62
78
|
interface InboundEmail {
|
|
@@ -67,8 +83,8 @@ interface InboundEmail {
|
|
|
67
83
|
* about, parsed from the receiving MX's **first/topmost**
|
|
68
84
|
* `Authentication-Results` header. SECURITY: see
|
|
69
85
|
* {@link InboundAuthentication} — `from` is spoofable, and an SPF/DKIM pass
|
|
70
|
-
* vouches for it only when
|
|
71
|
-
* pass checked that already). Reading the raw `headers["authentication-results"]`
|
|
86
|
+
* vouches for it only when that clause's `domain` equals the `From` domain (a
|
|
87
|
+
* DMARC pass checked that already). Reading the raw `headers["authentication-results"]`
|
|
72
88
|
* map instead exposes last-wins (a lower, potentially attacker-injected)
|
|
73
89
|
* value — trust `authentication`, not the raw map.
|
|
74
90
|
*/
|
|
@@ -98,6 +114,37 @@ interface InboundEmail {
|
|
|
98
114
|
* `Uint8Array`, or a decoded string.
|
|
99
115
|
*/
|
|
100
116
|
declare const parseInboundEmail: (raw: RawInboundEmail) => Promise<InboundEmail>;
|
|
117
|
+
/**
|
|
118
|
+
* THE inbound sender-authentication gate: does the receiving MX vouch for this
|
|
119
|
+
* message's `From` domain? Pass it as the `verify` hook of
|
|
120
|
+
* `createInboundEmailHandler` (or call it from your own) — it is one exported
|
|
121
|
+
* helper precisely so the insecure variants cannot be hand-rolled again.
|
|
122
|
+
*
|
|
123
|
+
* True when ANY reported DMARC, SPF or DKIM clause both **passes** and names a
|
|
124
|
+
* `domain` equal to the `From` address's domain. False otherwise — including for
|
|
125
|
+
* an empty verdict list (the MX stamped no `Authentication-Results` header at
|
|
126
|
+
* all, which is "unknown", not "fine") and for a `From` with no single mailbox
|
|
127
|
+
* to align against.
|
|
128
|
+
*
|
|
129
|
+
* SECURITY — the two halves are each load-bearing.
|
|
130
|
+
*
|
|
131
|
+
* **Alignment.** A bare `pass` proves nothing about `From`. SPF authenticates the
|
|
132
|
+
* envelope `MAIL FROM` domain and DKIM the signing `d=`, both attacker-chosen, so
|
|
133
|
+
* `spf=pass`+`dkim=pass` for `evil.example` is routine on a message whose `From`
|
|
134
|
+
* says `ceo@victim.example`. Only a clause whose own `domain` equals the `From`
|
|
135
|
+
* domain vouches for it. Alignment is STRICT (RFC 7489): there is no
|
|
136
|
+
* public-suffix list here, so `mail.example.com` does not vouch for
|
|
137
|
+
* `example.com`. A pass reporting no domain (`null`) cannot be aligned and is
|
|
138
|
+
* rejected. A DMARC pass already checked alignment at the MX.
|
|
139
|
+
*
|
|
140
|
+
* **Every clause, not the first.** One header legitimately reports a method more
|
|
141
|
+
* than once (an ESP-relayed message is DKIM-signed by both the relay and the
|
|
142
|
+
* author domain), and the aligned clause is not reliably the first, so reading
|
|
143
|
+
* only the first bounced fully authenticated mail. "Any clause passes AND aligns"
|
|
144
|
+
* stays strictly narrower than a bare pass: a clause vouching for some other
|
|
145
|
+
* domain contributes nothing.
|
|
146
|
+
*/
|
|
147
|
+
declare const authenticatesFrom: (email: InboundEmail) => boolean;
|
|
101
148
|
/**
|
|
102
149
|
* Structural projection of Cloudflare's `ForwardableEmailMessage` (verified
|
|
103
150
|
* against `@cloudflare/workers-types`' `ForwardableEmailMessage`). Only the
|
|
@@ -301,10 +348,11 @@ interface DispatchToLunoraFunctionOptions<TEnv = Record<string, unknown>> {
|
|
|
301
348
|
* and then hands the message to `retain` if one is configured, bouncing only
|
|
302
349
|
* when there is nowhere durable to put it (see {@link createInboundEmailHandler}).
|
|
303
350
|
*
|
|
304
|
-
* SECURITY: the RPC
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
351
|
+
* SECURITY: the RPC is marked a trusted system dispatch, so `functionPath` may
|
|
352
|
+
* (and should) name an `internalMutation`/`internalAction` — a public `mutation`
|
|
353
|
+
* target is callable by any browser client with a forged message. The input is
|
|
354
|
+
* fully attacker-controlled and spoofable; verify the sender (`verify` hook /
|
|
355
|
+
* `email.authentication`) before making any trust decision in the target.
|
|
308
356
|
*/
|
|
309
357
|
declare const dispatchToLunoraFunction: <TEnv extends Record<string, unknown> = Record<string, unknown>>(options: DispatchToLunoraFunctionOptions<TEnv>) => InboundDispatch<TEnv>;
|
|
310
358
|
export {
|
|
@@ -339,7 +387,7 @@ type DispatchToLunoraFunctionOptions,
|
|
|
339
387
|
* });
|
|
340
388
|
* ```
|
|
341
389
|
*/
|
|
342
|
-
type ForwardableEmailMessageLike, type InboundAttachment, type InboundAuthentication,
|
|
390
|
+
type ForwardableEmailMessageLike, type InboundAttachment, type InboundAuthResult, type InboundAuthentication,
|
|
343
391
|
/**
|
|
344
392
|
* `@lunora/mail/inbound` — inbound Email Routing support.
|
|
345
393
|
*
|
|
@@ -435,4 +483,4 @@ type InboundVerify, type RawInboundEmail,
|
|
|
435
483
|
* });
|
|
436
484
|
* ```
|
|
437
485
|
*/
|
|
438
|
-
type RpcEnvelope, type ShardNamespaceLike, createInboundEmailHandler, dispatchToLunoraFunction, parseInboundEmail };
|
|
486
|
+
type RpcEnvelope, type ShardNamespaceLike, authenticatesFrom, createInboundEmailHandler, dispatchToLunoraFunction, parseInboundEmail };
|
package/dist/inbound/index.d.ts
CHANGED
|
@@ -15,6 +15,18 @@ interface InboundAttachment {
|
|
|
15
15
|
/** Declared MIME type (e.g. `image/png`). */
|
|
16
16
|
mimeType: string;
|
|
17
17
|
}
|
|
18
|
+
/** One reported `method=result [ptype.property=value]` clause of an `Authentication-Results` header. */
|
|
19
|
+
interface InboundAuthResult {
|
|
20
|
+
/**
|
|
21
|
+
* The identifier the result is about, lowercased — DKIM's signing domain
|
|
22
|
+
* (`header.d=`), SPF's envelope `MAIL FROM` domain (`smtp.mailfrom=`, local
|
|
23
|
+
* part dropped) or DMARC's `header.from=`. `null` when the clause reported
|
|
24
|
+
* none, in which case it cannot be aligned and vouches for nothing.
|
|
25
|
+
*/
|
|
26
|
+
domain: string | null;
|
|
27
|
+
/** The result token, lowercased (`"pass"`, `"fail"`, `"none"`, …). */
|
|
28
|
+
result: string;
|
|
29
|
+
}
|
|
18
30
|
/**
|
|
19
31
|
* Sender-authentication verdicts pulled from the `Authentication-Results` header
|
|
20
32
|
* the receiving MX (e.g. Cloudflare Email Routing) stamped on the message.
|
|
@@ -24,17 +36,27 @@ interface InboundAttachment {
|
|
|
24
36
|
* spoofable, so a downstream handler MUST NOT make trust/authorization decisions
|
|
25
37
|
* on `email.from` alone — gate on these verdicts (or your own policy) instead.
|
|
26
38
|
* Verdicts are best-effort: when the receiving MX did not stamp an
|
|
27
|
-
* `Authentication-Results` header, every
|
|
39
|
+
* `Authentication-Results` header, every list is empty ("unknown").
|
|
40
|
+
*
|
|
41
|
+
* Each method holds a LIST, because RFC 8601 lets one header report the same
|
|
42
|
+
* method more than once and real mail does. An ESP-relayed message carries two
|
|
43
|
+
* DKIM signatures — the relay's and the author domain's — and the MX stamps a
|
|
44
|
+
* clause per signature in whatever order it verified them. Keeping only the
|
|
45
|
+
* first threw the aligned one away whenever it was not the one that happened to
|
|
46
|
+
* come first, and the message was rejected as unauthenticated. A consumer
|
|
47
|
+
* therefore asks "does ANY reported clause pass and align?", not "did the first
|
|
48
|
+
* one?".
|
|
28
49
|
*
|
|
29
50
|
* SECURITY: a bare `"pass"` is NOT proof the `From` header is genuine. SPF
|
|
30
51
|
* authenticates the envelope `MAIL FROM` domain and DKIM the signing domain
|
|
31
52
|
* (`d=`), and an attacker controls both — `spf=pass smtp.mailfrom=evil.example;
|
|
32
53
|
* dkim=pass header.d=evil.example` is routine for a message whose `From` says
|
|
33
54
|
* `ceo@victim.example`. Each verdict therefore carries the identifier it is
|
|
34
|
-
* about (
|
|
35
|
-
* domain equals the `From` address's domain (RFC 7489 strict
|
|
36
|
-
* DMARC pass already checked alignment for you. A pass with
|
|
37
|
-
* reported cannot be aligned and must be treated as
|
|
55
|
+
* about ({@link InboundAuthResult.domain}): an SPF or DKIM pass only vouches for
|
|
56
|
+
* `from` when that domain equals the `From` address's domain (RFC 7489 strict
|
|
57
|
+
* alignment). Only a DMARC pass already checked alignment for you. A pass with
|
|
58
|
+
* no identifier reported cannot be aligned and must be treated as
|
|
59
|
+
* unauthenticated.
|
|
38
60
|
*
|
|
39
61
|
* SECURITY: verdicts are read from the **first/topmost** `Authentication-Results`
|
|
40
62
|
* header in document order. The receiving MX prepends its own genuine header per
|
|
@@ -45,18 +67,12 @@ interface InboundAttachment {
|
|
|
45
67
|
* config this runtime-agnostic parser does not carry, so it is left to the host.
|
|
46
68
|
*/
|
|
47
69
|
interface InboundAuthentication {
|
|
48
|
-
/** DKIM
|
|
49
|
-
dkim:
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
/** `From` domain the DMARC verdict evaluated (`header.from=`, lowercased), or `null` when not reported. */
|
|
55
|
-
dmarcDomain: string | null;
|
|
56
|
-
/** SPF verdict (`"pass"`/`"fail"`/…), or `null` when not reported. */
|
|
57
|
-
spf: string | null;
|
|
58
|
-
/** Envelope `MAIL FROM` domain the SPF verdict is about (`smtp.mailfrom=`, local part dropped, lowercased), or `null` when not reported. */
|
|
59
|
-
spfDomain: string | null;
|
|
70
|
+
/** Every DKIM clause the header reported, in header order; empty when the method was not reported. */
|
|
71
|
+
dkim: InboundAuthResult[];
|
|
72
|
+
/** Every DMARC clause the header reported, in header order; empty when the method was not reported. */
|
|
73
|
+
dmarc: InboundAuthResult[];
|
|
74
|
+
/** Every SPF clause the header reported, in header order; empty when the method was not reported. */
|
|
75
|
+
spf: InboundAuthResult[];
|
|
60
76
|
}
|
|
61
77
|
/** Normalised, transport-agnostic view of a received message. */
|
|
62
78
|
interface InboundEmail {
|
|
@@ -67,8 +83,8 @@ interface InboundEmail {
|
|
|
67
83
|
* about, parsed from the receiving MX's **first/topmost**
|
|
68
84
|
* `Authentication-Results` header. SECURITY: see
|
|
69
85
|
* {@link InboundAuthentication} — `from` is spoofable, and an SPF/DKIM pass
|
|
70
|
-
* vouches for it only when
|
|
71
|
-
* pass checked that already). Reading the raw `headers["authentication-results"]`
|
|
86
|
+
* vouches for it only when that clause's `domain` equals the `From` domain (a
|
|
87
|
+
* DMARC pass checked that already). Reading the raw `headers["authentication-results"]`
|
|
72
88
|
* map instead exposes last-wins (a lower, potentially attacker-injected)
|
|
73
89
|
* value — trust `authentication`, not the raw map.
|
|
74
90
|
*/
|
|
@@ -98,6 +114,37 @@ interface InboundEmail {
|
|
|
98
114
|
* `Uint8Array`, or a decoded string.
|
|
99
115
|
*/
|
|
100
116
|
declare const parseInboundEmail: (raw: RawInboundEmail) => Promise<InboundEmail>;
|
|
117
|
+
/**
|
|
118
|
+
* THE inbound sender-authentication gate: does the receiving MX vouch for this
|
|
119
|
+
* message's `From` domain? Pass it as the `verify` hook of
|
|
120
|
+
* `createInboundEmailHandler` (or call it from your own) — it is one exported
|
|
121
|
+
* helper precisely so the insecure variants cannot be hand-rolled again.
|
|
122
|
+
*
|
|
123
|
+
* True when ANY reported DMARC, SPF or DKIM clause both **passes** and names a
|
|
124
|
+
* `domain` equal to the `From` address's domain. False otherwise — including for
|
|
125
|
+
* an empty verdict list (the MX stamped no `Authentication-Results` header at
|
|
126
|
+
* all, which is "unknown", not "fine") and for a `From` with no single mailbox
|
|
127
|
+
* to align against.
|
|
128
|
+
*
|
|
129
|
+
* SECURITY — the two halves are each load-bearing.
|
|
130
|
+
*
|
|
131
|
+
* **Alignment.** A bare `pass` proves nothing about `From`. SPF authenticates the
|
|
132
|
+
* envelope `MAIL FROM` domain and DKIM the signing `d=`, both attacker-chosen, so
|
|
133
|
+
* `spf=pass`+`dkim=pass` for `evil.example` is routine on a message whose `From`
|
|
134
|
+
* says `ceo@victim.example`. Only a clause whose own `domain` equals the `From`
|
|
135
|
+
* domain vouches for it. Alignment is STRICT (RFC 7489): there is no
|
|
136
|
+
* public-suffix list here, so `mail.example.com` does not vouch for
|
|
137
|
+
* `example.com`. A pass reporting no domain (`null`) cannot be aligned and is
|
|
138
|
+
* rejected. A DMARC pass already checked alignment at the MX.
|
|
139
|
+
*
|
|
140
|
+
* **Every clause, not the first.** One header legitimately reports a method more
|
|
141
|
+
* than once (an ESP-relayed message is DKIM-signed by both the relay and the
|
|
142
|
+
* author domain), and the aligned clause is not reliably the first, so reading
|
|
143
|
+
* only the first bounced fully authenticated mail. "Any clause passes AND aligns"
|
|
144
|
+
* stays strictly narrower than a bare pass: a clause vouching for some other
|
|
145
|
+
* domain contributes nothing.
|
|
146
|
+
*/
|
|
147
|
+
declare const authenticatesFrom: (email: InboundEmail) => boolean;
|
|
101
148
|
/**
|
|
102
149
|
* Structural projection of Cloudflare's `ForwardableEmailMessage` (verified
|
|
103
150
|
* against `@cloudflare/workers-types`' `ForwardableEmailMessage`). Only the
|
|
@@ -301,10 +348,11 @@ interface DispatchToLunoraFunctionOptions<TEnv = Record<string, unknown>> {
|
|
|
301
348
|
* and then hands the message to `retain` if one is configured, bouncing only
|
|
302
349
|
* when there is nowhere durable to put it (see {@link createInboundEmailHandler}).
|
|
303
350
|
*
|
|
304
|
-
* SECURITY: the RPC
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
351
|
+
* SECURITY: the RPC is marked a trusted system dispatch, so `functionPath` may
|
|
352
|
+
* (and should) name an `internalMutation`/`internalAction` — a public `mutation`
|
|
353
|
+
* target is callable by any browser client with a forged message. The input is
|
|
354
|
+
* fully attacker-controlled and spoofable; verify the sender (`verify` hook /
|
|
355
|
+
* `email.authentication`) before making any trust decision in the target.
|
|
308
356
|
*/
|
|
309
357
|
declare const dispatchToLunoraFunction: <TEnv extends Record<string, unknown> = Record<string, unknown>>(options: DispatchToLunoraFunctionOptions<TEnv>) => InboundDispatch<TEnv>;
|
|
310
358
|
export {
|
|
@@ -339,7 +387,7 @@ type DispatchToLunoraFunctionOptions,
|
|
|
339
387
|
* });
|
|
340
388
|
* ```
|
|
341
389
|
*/
|
|
342
|
-
type ForwardableEmailMessageLike, type InboundAttachment, type InboundAuthentication,
|
|
390
|
+
type ForwardableEmailMessageLike, type InboundAttachment, type InboundAuthResult, type InboundAuthentication,
|
|
343
391
|
/**
|
|
344
392
|
* `@lunora/mail/inbound` — inbound Email Routing support.
|
|
345
393
|
*
|
|
@@ -435,4 +483,4 @@ type InboundVerify, type RawInboundEmail,
|
|
|
435
483
|
* });
|
|
436
484
|
* ```
|
|
437
485
|
*/
|
|
438
|
-
type RpcEnvelope, type ShardNamespaceLike, createInboundEmailHandler, dispatchToLunoraFunction, parseInboundEmail };
|
|
486
|
+
type RpcEnvelope, type ShardNamespaceLike, authenticatesFrom, createInboundEmailHandler, dispatchToLunoraFunction, parseInboundEmail };
|
package/dist/inbound/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createInboundEmailHandler as
|
|
1
|
+
import{createInboundEmailHandler as n,dispatchToLunoraFunction as r}from"../packem_shared/createInboundEmailHandler-vXSNL5mZ.mjs";import{authenticatesFrom as t,parseInboundEmail as i}from"../packem_shared/authenticatesFrom-xpKAFMRN.mjs";export{t as authenticatesFrom,n as createInboundEmailHandler,r as dispatchToLunoraFunction,i as parseInboundEmail};
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createCaptureTransport as a}from"./packem_shared/createCaptureTransport-CKc6NpZR.mjs";import{createCloudflareTransport as t}from"./packem_shared/createCloudflareTransport-DpfCxn16.mjs";import{default as u}from"./packem_shared/createMailer-
|
|
1
|
+
import{createCaptureTransport as a}from"./packem_shared/createCaptureTransport-CKc6NpZR.mjs";import{createCloudflareTransport as t}from"./packem_shared/createCloudflareTransport-DpfCxn16.mjs";import{default as u}from"./packem_shared/createMailer-CjQmT7Ln.mjs";import{createCaptureSink as f,createMailerFromEnv as l,shouldCaptureMail as m}from"./packem_shared/createCaptureSink-wbDNGuA-.mjs";import{consumeQueuedSend as s,toQueuedPayload as c}from"./packem_shared/consumeQueuedSend-CRfALnCU.mjs";import{default as i}from"./packem_shared/renderEmail-BOUnr6i3.mjs";import{default as M}from"./packem_shared/createResendTransport-DDkOitck.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 @@
|
|
|
1
|
+
import p from"postal-mime";import{a as c}from"./address-soPj2Z0j.mjs";const d=(s,e)=>{if(e!==void 0)return c(`inbound ${s}`,e),e},u=s=>s.address!==void 0&&s.address!==""?s.name?`${s.name} <${s.address}>`:s.address:s.group?s.group.map(e=>e.address??"").filter(e=>e!=="").join(", "):s.name??"",l=/"(?:[^"\\]|\\.)*(?:"|$)|\((?:[^()\\]|\\.)*(?:\)|$)/g,g=s=>s.replaceAll(l,e=>e.startsWith('"')?e.replaceAll(/[;=]/gu," "):" "),m=(s,e,r)=>{const i=g(s).matchAll(new RegExp(String.raw`(?:^|;)\s*${e}\s*(?:/\s*\d+\s*)?=\s*([a-z]+)([^;]*)`,"gi")),a=new RegExp(String.raw`\b${r.split(".").join(String.raw`\s*\.\s*`)}\s*=\s*"?([^\s;"]+)`,"i");return[...i].map(t=>{const n=a.exec(t[2]??"")?.[1];return{domain:n===void 0?null:n.slice(n.lastIndexOf("@")+1).toLowerCase(),result:(t[1]??"").toLowerCase()}})},v=s=>s===void 0||s===""?{dkim:[],dmarc:[],spf:[]}:{dkim:m(s,"dkim","header.d"),dmarc:m(s,"dmarc","header.from"),spf:m(s,"spf","smtp.mailfrom")},I=async s=>{const e=await p.parse(s),r={};for(const o of e.headers)c(`inbound header \`${o.key}\``,o.value),r[o.key]=o.value;const i=e.headers.find(o=>o.key==="authentication-results")?.value,a=(e.to??[]).map(o=>{const f=u(o);return c("inbound to",f),f}),t=e.from?u(e.from):"";return c("inbound from",t),{attachments:e.attachments.map(o=>({content:o.content,disposition:o.disposition,...o.encoding===void 0?{}:{encoding:o.encoding},filename:o.filename,mimeType:o.mimeType})),authentication:v(i),from:t,headers:r,...e.html===void 0?{}:{html:e.html},...d("inReplyTo",e.inReplyTo)===void 0?{}:{inReplyTo:e.inReplyTo},...d("messageId",e.messageId)===void 0?{}:{messageId:e.messageId},...d("references",e.references)===void 0?{}:{references:e.references},...d("subject",e.subject)===void 0?{}:{subject:e.subject},...e.text===void 0?{}:{text:e.text},to:a}},h=/<([^<>]*)>$/,x=s=>{const e=h.exec(s)?.[1]??s,r=e.indexOf("@");if(!(r===-1||r!==e.lastIndexOf("@")))return e.slice(r+1).trim().toLowerCase()},$=s=>{const e=x(s.from);if(e===void 0)return!1;const r=n=>n.some(o=>o.result==="pass"&&o.domain===e),{dkim:i,dmarc:a,spf:t}=s.authentication;return r(a)||r(t)||r(i)};export{$ as authenticatesFrom,I as parseInboundEmail};
|
package/dist/packem_shared/{createCaptureSink-4T2LJOYC.mjs → createCaptureSink-wbDNGuA-.mjs}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createCaptureTransport as d}from"./createCaptureTransport-CKc6NpZR.mjs";import"./createCloudflareTransport-DpfCxn16.mjs";import n from"./createMailer-
|
|
1
|
+
import{createCaptureTransport as d}from"./createCaptureTransport-CKc6NpZR.mjs";import"./createCloudflareTransport-DpfCxn16.mjs";import n from"./createMailer-CjQmT7Ln.mjs";import{LunoraError as c}from"@lunora/errors";import{D as s,p as l}from"./shard-CRhAKK93.mjs";import"./consumeQueuedSend-CRfALnCU.mjs";import"./renderEmail-BOUnr6i3.mjs";import"./createResendTransport-DDkOitck.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};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{LunoraError as d}from"@lunora/errors";import{D as h,p as l}from"./shard-
|
|
1
|
+
import{LunoraError as d}from"@lunora/errors";import{D as h,p as l}from"./shard-CRhAKK93.mjs";const f=r=>{let n="";for(let e=0;e<r.length;e+=32768)n+=String.fromCharCode(...r.subarray(e,e+32768));return btoa(n)},E="message could not be processed",u=(r,n)=>{console.error("@lunora/mail/inbound: dropping message —",r),n.message.setReject(E)},y=r=>{const n=r.onError??u;return async(t,e,c)=>{const a={ctx:c,env:e,message:t};let o;try{if(o=await r.parse(t.raw),r.verify&&await r.verify(o,a)===!1)throw new d("INTERNAL","@lunora/mail/inbound: sender verification rejected the message")}catch(i){await n(i,a);return}try{await r.dispatch(o,a)}catch(i){if(r.onError)try{await r.onError(i,a)}catch(s){console.error("@lunora/mail/inbound: onError threw while reporting a dispatch failure —",s)}if(r.retain)try{await r.retain(o,a,i);return}catch(s){console.error("@lunora/mail/inbound: retain failed to take ownership of the message —",s)}u(i,a)}}},m=r=>r.attachments.length===0?r:{...r,attachments:r.attachments.map(n=>{const{content:t}=n;if(typeof t=="string")return n;const e=t instanceof Uint8Array?t:new Uint8Array(t);return{...n,content:f(e),encoding:"base64"}})},A=r=>{const n=r.shardKey??h,t=r.resolveArgs??(e=>m(e));return async(e,c)=>{const a=r.adminToken??(typeof c.env.LUNORA_ADMIN_TOKEN=="string"?c.env.LUNORA_ADMIN_TOKEN:void 0);if(a===void 0||a==="")throw new d("INTERNAL","@lunora/mail/inbound: missing LUNORA_ADMIN_TOKEN — cannot authorize inbound dispatch to the shard RPC.");const o={args:t(e,c),functionPath:r.functionPath,shardKey:n};await l(r.shard,{adminToken:a,envelope:o,jurisdiction:r.jurisdiction,label:`@lunora/mail/inbound: dispatch to \`${r.functionPath}\``,shardKey:n})}};export{y as createInboundEmailHandler,A as dispatchToLunoraFunction};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as f}from"@lunora/errors";import{a as o,c as m}from"./address-soPj2Z0j.mjs";import{isCaptureTransport as s}from"./createCaptureTransport-CKc6NpZR.mjs";import{createCloudflareTransport as y}from"./createCloudflareTransport-DpfCxn16.mjs";import{toQueuedPayload as i}from"./consumeQueuedSend-CRfALnCU.mjs";import b from"./renderEmail-BOUnr6i3.mjs";import w from"./createResendTransport-DDkOitck.mjs";const h=()=>{if(typeof crypto<"u"){if(typeof crypto.randomUUID=="function")return crypto.randomUUID();if(typeof crypto.getRandomValues=="function"){const r=crypto.getRandomValues(new Uint8Array(16));return Array.from(r,d=>d.toString(16).padStart(2,"0")).join("")}}throw new Error("randomSessionId: no Web Crypto available — a session id needs crypto.randomUUID or crypto.getRandomValues")},p=r=>{if(r.cloudflareSend)return y({from:r.from,send:r.cloudflareSend});if(r.apiKey)return w(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)")},N=r=>{if(!r.from)throw new f("INTERNAL","@lunora/mail: `from` is required");const d=r.transport??p(r),n=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(o("subject",e.subject),e.headers)for(const[t,l]of Object.entries(e.headers))o(`header name "${t}"`,t),o(`header "${t}" value`,l);const c=e.from??r.from;return m({bcc:e.bcc,cc:e.cc,from:c,replyTo:e.replyTo,to:e.to}),{bcc:e.bcc,cc:e.cc,from:c,headers:e.headers,html:a,replyTo:e.replyTo,subject:e.subject,text:u,to:e.to}};return{queue:async e=>{if(!r.queue){if(s(d)){const c=await n(e);return await d.send(c),{queued:!0}}throw new f("INTERNAL","@lunora/mail: `queue` binding is required for mailer.queue()")}const a=await n(e),u=e.idempotencyKey??h();return await r.queue.send(i({...a,idempotencyKey:u})),{queued:!0}},send:async e=>{const a=await n(e);return d.send(a)}}};export{N as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as s}from"@lunora/errors";const d="__root__",a=(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=a(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","x-lunora-system":"1"},method:"POST"});if(e.ok===!1)throw new s("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 s("INTERNAL",`${r.label} returned an error: ${JSON.stringify(n)}`)}return o};export{d as D,l as p};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/mail",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.62",
|
|
4
4
|
"description": "Email for Lunora: Resend adapter, TSX templates, and queue-backed sends",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"access": "public"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
57
|
+
"@lunora/errors": "1.0.0-alpha.31",
|
|
58
58
|
"@react-email/render": "2.1.0",
|
|
59
59
|
"@visulima/email": "2.1.8",
|
|
60
60
|
"postal-mime": "2.7.6"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as l}from"@lunora/errors";import{a as f,c as m}from"./address-soPj2Z0j.mjs";import{isCaptureTransport as s}from"./createCaptureTransport-CKc6NpZR.mjs";import{createCloudflareTransport as y}from"./createCloudflareTransport-DpfCxn16.mjs";import{toQueuedPayload as i}from"./consumeQueuedSend-CRfALnCU.mjs";import w from"./renderEmail-BOUnr6i3.mjs";import b from"./createResendTransport-DDkOitck.mjs";const h=()=>{if(typeof crypto<"u"){if(typeof crypto.randomUUID=="function")return crypto.randomUUID();if(typeof crypto.getRandomValues=="function"){const r=crypto.getRandomValues(new Uint8Array(16));return Array.from(r,u=>u.toString(16).padStart(2,"0")).join("")}}return Date.now().toString(36)},q=r=>{if(r.cloudflareSend)return y({from:r.from,send:r.cloudflareSend});if(r.apiKey)return b(r.apiKey,r.from);throw new l("INTERNAL","@lunora/mail: a transport is required — pass `transport`, `cloudflareSend` (Cloudflare Email Workers, the default), or `apiKey` (Resend)")},g=r=>{if(!r.from)throw new l("INTERNAL","@lunora/mail: `from` is required");const u=r.transport??q(r),n=async e=>{let{html:a}=e,{text:c}=e;if(e.react){const t=await w(e.react);a=a??t.html,c=c??t.text}if(f("subject",e.subject),e.headers)for(const[t,o]of Object.entries(e.headers))f(`header name "${t}"`,t),f(`header "${t}" value`,o);const d=e.from??r.from;return m({bcc:e.bcc,cc:e.cc,from:d,replyTo:e.replyTo,to:e.to}),{bcc:e.bcc,cc:e.cc,from:d,headers:e.headers,html:a,replyTo:e.replyTo,subject:e.subject,text:c,to:e.to}};return{queue:async e=>{if(!r.queue){if(s(u)){const d=await n(e);return await u.send(d),{queued:!0}}throw new l("INTERNAL","@lunora/mail: `queue` binding is required for mailer.queue()")}const a=await n(e),c=e.idempotencyKey??h();return await r.queue.send(i({...a,idempotencyKey:c})),{queued:!0}},send:async e=>{const a=await n(e);return u.send(a)}}};export{g as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import f from"postal-mime";import{a as d}from"./address-soPj2Z0j.mjs";const t=(n,e)=>{if(e!==void 0)return d(`inbound ${n}`,e),e},u=n=>n.address!==void 0&&n.address!==""?n.name?`${n.name} <${n.address}>`:n.address:n.group?n.group.map(e=>e.address??"").filter(e=>e!=="").join(", "):n.name??"",m=(n,e,i)=>{const s=new RegExp(String.raw`(?:^|;)\s*${e}\s*=\s*([a-z]+)([^;]*)`,"i").exec(n.replaceAll(/\([^()]*\)/g,""));if(!s)return[null,null];const r=new RegExp(String.raw`\b${i.replaceAll(".",String.raw`\.`)}\s*=\s*"?([^\s;"]+)`,"i").exec(s[2]??"")?.[1];return[s[1]?.toLowerCase()??null,r===void 0?null:r.slice(r.lastIndexOf("@")+1).toLowerCase()]},p=n=>{if(n===void 0||n==="")return{dkim:null,dkimDomain:null,dmarc:null,dmarcDomain:null,spf:null,spfDomain:null};const[e,i]=m(n,"dkim","header.d"),[s,r]=m(n,"dmarc","header.from"),[a,c]=m(n,"spf","smtp.mailfrom");return{dkim:e,dkimDomain:i,dmarc:s,dmarcDomain:r,spf:a,spfDomain:c}},b=async n=>{const e=await f.parse(n),i={};for(const o of e.headers)d(`inbound header \`${o.key}\``,o.value),i[o.key]=o.value;const s=e.headers.find(o=>o.key==="authentication-results")?.value,r=(e.to??[]).map(o=>{const l=u(o);return d("inbound to",l),l}),a=e.from?u(e.from):"";return d("inbound from",a),{attachments:e.attachments.map(o=>({content:o.content,disposition:o.disposition,...o.encoding===void 0?{}:{encoding:o.encoding},filename:o.filename,mimeType:o.mimeType})),authentication:p(s),from:a,headers:i,...e.html===void 0?{}:{html:e.html},...t("inReplyTo",e.inReplyTo)===void 0?{}:{inReplyTo:e.inReplyTo},...t("messageId",e.messageId)===void 0?{}:{messageId:e.messageId},...t("references",e.references)===void 0?{}:{references:e.references},...t("subject",e.subject)===void 0?{}:{subject:e.subject},...e.text===void 0?{}:{text:e.text},to:r}};export{b as parseInboundEmail};
|
|
@@ -1 +0,0 @@
|
|
|
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};
|