@lunora/mail 1.0.0-alpha.17 → 1.0.0-alpha.19
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.mjs +1 -2
- package/dist/index.mjs +1 -7
- package/dist/packem_shared/address-Y4Z--6ZO.mjs +3 -0
- package/dist/packem_shared/consumeQueuedSend-BXrPhND5.mjs +1 -0
- package/dist/packem_shared/createCaptureSink-fVjCTUBC.mjs +1 -0
- package/dist/packem_shared/createCaptureTransport-BPdMkU0O.mjs +1 -0
- package/dist/packem_shared/createCloudflareTransport-V23CVw7F.mjs +1 -0
- package/dist/packem_shared/createInboundEmailHandler-B7H4h_sK.mjs +1 -0
- package/dist/packem_shared/createMailer-CsVLrc75.mjs +1 -0
- package/dist/packem_shared/createResendTransport-CpSJ4j7C.mjs +1 -0
- package/dist/packem_shared/parseInboundEmail-tJMs3YON.mjs +1 -0
- package/dist/packem_shared/provider-transport-BZngzyeS.mjs +1 -0
- package/dist/packem_shared/renderEmail-DM8_pWKa.mjs +1 -0
- package/dist/packem_shared/shard-CcKsuWAm.mjs +1 -0
- package/dist/testing.mjs +1 -65
- package/package.json +3 -3
- package/dist/packem_shared/address-vSUAVU2T.mjs +0 -64
- package/dist/packem_shared/consumeQueuedSend-B9hTDOZ6.mjs +0 -77
- package/dist/packem_shared/createCaptureSink-B0c7fQJf.mjs +0 -77
- package/dist/packem_shared/createCaptureTransport-Crz_8822.mjs +0 -11
- package/dist/packem_shared/createCloudflareTransport-DXh1nUNi.mjs +0 -28
- package/dist/packem_shared/createInboundEmailHandler-BMB5t-qh.mjs +0 -74
- package/dist/packem_shared/createMailer-B4Z_Cgiu.mjs +0 -81
- package/dist/packem_shared/createResendTransport-CMQWjZoi.mjs +0 -16
- package/dist/packem_shared/parseInboundEmail-DXVOH0wz.mjs +0 -73
- package/dist/packem_shared/provider-transport-C0xHi3oy.mjs +0 -48
- package/dist/packem_shared/renderEmail-hyS1bpVP.mjs +0 -8
- package/dist/packem_shared/shard-DEDcQoFg.mjs +0 -36
package/dist/inbound/index.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { parseInboundEmail } from '../packem_shared/parseInboundEmail-DXVOH0wz.mjs';
|
|
1
|
+
import{createInboundEmailHandler as r,dispatchToLunoraFunction as a}from"../packem_shared/createInboundEmailHandler-B7H4h_sK.mjs";import{parseInboundEmail as t}from"../packem_shared/parseInboundEmail-tJMs3YON.mjs";export{r as createInboundEmailHandler,a as dispatchToLunoraFunction,t as parseInboundEmail};
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { createCloudflareTransport } from './packem_shared/createCloudflareTransport-DXh1nUNi.mjs';
|
|
3
|
-
export { default as createMailer } from './packem_shared/createMailer-B4Z_Cgiu.mjs';
|
|
4
|
-
export { createCaptureSink, createMailerFromEnv, shouldCaptureMail } from './packem_shared/createCaptureSink-B0c7fQJf.mjs';
|
|
5
|
-
export { consumeQueuedSend, toQueuedPayload } from './packem_shared/consumeQueuedSend-B9hTDOZ6.mjs';
|
|
6
|
-
export { default as renderEmail } from './packem_shared/renderEmail-hyS1bpVP.mjs';
|
|
7
|
-
export { default as createResendTransport } from './packem_shared/createResendTransport-CMQWjZoi.mjs';
|
|
1
|
+
import{createCaptureTransport as a}from"./packem_shared/createCaptureTransport-BPdMkU0O.mjs";import{createCloudflareTransport as t}from"./packem_shared/createCloudflareTransport-V23CVw7F.mjs";import{default as u}from"./packem_shared/createMailer-CsVLrc75.mjs";import{createCaptureSink as f,createMailerFromEnv as l,shouldCaptureMail as m}from"./packem_shared/createCaptureSink-fVjCTUBC.mjs";import{consumeQueuedSend as s,toQueuedPayload as c}from"./packem_shared/consumeQueuedSend-BXrPhND5.mjs";import{default as i}from"./packem_shared/renderEmail-DM8_pWKa.mjs";import{default as M}from"./packem_shared/createResendTransport-CpSJ4j7C.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 i}from"@lunora/errors";const r=320,l=256,s=/^([^<]*)<([^>]*)>\s*$/,o=(e,a)=>{if(a.includes("\r")||a.includes(`
|
|
2
|
+
`)||a.includes(","))throw new i("INTERNAL",`@lunora/mail: address ${e} must not contain CR, LF, or comma`)},h=(e,a)=>{if(a.includes("\r")||a.includes(`
|
|
3
|
+
`))throw new i("INTERNAL",`@lunora/mail: ${e} must not contain CR or LF`)},c=(e,a)=>{if(e.length>l)throw new i("INTERNAL",`@lunora/mail: address name must be <= ${String(l)} characters`);if(a.length>r)throw new i("INTERNAL",`@lunora/mail: address email must be <= ${String(r)} characters`);return e&&o("name",e),o("email",a),e?{email:a,name:e}:{email:a}},u=e=>{const a=e.trim();if(a.length>r)throw new i("INTERNAL",`@lunora/mail: address email must be <= ${String(r)} characters`);return o("email",a),{email:a}},n=e=>{const a=s.exec(e),m=(a?.[2]??"").trim();return a&&m?c((a[1]??"").trim(),m):u(e)},t=e=>e===void 0?void 0:(Array.isArray(e)?e:[e]).map(a=>n(a)),w=e=>{t(e.to),t(e.cc),t(e.bcc),e.from!==void 0&&n(e.from),e.replyTo!==void 0&&n(e.replyTo)};export{w as f,t as i,h as m,n as o};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as c}from"@lunora/errors";const h=t=>({bcc:t.bcc,cc:t.cc,from:t.from,headers:t.headers,html:t.html,replyTo:t.replyTo,subject:t.subject,text:t.text,to:t.to}),b=async(t,s)=>{if(!s||typeof s!="object"||Array.isArray(s))throw new c("INTERNAL","@lunora/mail: queue message body must be an object");const e=s;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(o=>typeof o=="string");if(!m&&!y)throw new c("INTERNAL","@lunora/mail: queue message `to` must be a string or string[]");const a=(o,r)=>{if(r!==void 0){if(typeof r!="string")throw new TypeError(`@lunora/mail: queue message \`${o}\` must be a string`);return r}},u=(o,r)=>{if(r!==void 0){if(typeof r=="string")return[r];if(Array.isArray(r)&&r.every(n=>typeof n=="string"))return r;throw new TypeError(`@lunora/mail: queue message \`${o}\` must be a string or string[]`)}};let i;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 o=Object.entries(e.headers);for(const[r,n]of o)if(typeof n!="string")throw new TypeError(`@lunora/mail: queue message header "${r}" must be a string`);i=e.headers}const f={bcc:u("bcc",e.bcc),cc:u("cc",e.cc),from:a("from",e.from),headers:i,html:a("html",e.html),replyTo:a("replyTo",e.replyTo),subject:e.subject,text:a("text",e.text),to:e.to};return t.send(f)};export{b as consumeQueuedSend,h as toQueuedPayload};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as u}from"@lunora/errors";import{createCaptureTransport as s}from"./createCaptureTransport-BPdMkU0O.mjs";import n from"./createMailer-CsVLrc75.mjs";import{d as l,u as c}from"./shard-CcKsuWAm.mjs";const f="__lunora_admin__:recordMail",p=/^(?:dev(?:elopment)?|local(?:host)?|test)$/iu,m=["CF_ENV","ENVIRONMENT","NODE_ENV","WORKER_ENV"],E=(e,t)=>{const r=e[t];if(typeof r!="string"||r==="")throw new u("INTERNAL",`@lunora/mail: missing env var \`${t}\` — set it in .dev.vars (and \`wrangler secret put ${t}\` for secrets).`);return r},_=e=>{const t=e.LUNORA_MAIL_CAPTURE;if(typeof t=="string"){const r=t.toLowerCase();if(r==="1"||r==="true")return!0;if(r==="0"||r==="false")return!1;console.warn(`@lunora/mail: unrecognized LUNORA_MAIL_CAPTURE value "${t}" — expected "1"/"true" or "0"/"false"; falling back to environment detection.`)}return m.some(r=>{const o=e[r];return typeof o=="string"&&p.test(o)})},N=(e,t=l,r)=>({record:async o=>{const a=e.SHARD,i=typeof e.LUNORA_ADMIN_TOKEN=="string"?e.LUNORA_ADMIN_TOKEN:void 0;if(a===void 0||i===void 0)return{id:"uncaptured"};try{return{id:(await c(a,{adminToken:i,envelope:{args:o,functionPath:f},jurisdiction:r,label:"@lunora/mail: recording captured mail",shardKey:t})).result?.id??"captured"}}catch(d){return console.error("@lunora/mail: failed to record captured mail into the studio inbox —",d),{id:"uncaptured"}}}}),I=(e,t={})=>{const r=E(e,"MAIL_FROM");if(_(e))return n({from:r,transport:s(N(e,t.rootShard,t.jurisdiction))});if(t.cloudflareSend)return n({cloudflareSend:t.cloudflareSend,from:r});const o=typeof e.RESEND_API_KEY=="string"?e.RESEND_API_KEY:void 0;if(o!==void 0&&o!=="")return n({apiKey:o,from:r});throw new u("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{N as createCaptureSink,I as createMailerFromEnv,_ as shouldCaptureMail};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const a=Symbol.for("@lunora/mail.captureTransport"),t=r=>r[a]===!0,o=r=>({[a]:!0,send:async e=>r.record(e)});export{a as CAPTURE_TRANSPORT_BRAND,o as createCaptureTransport,t as isCaptureTransport};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as t}from"@lunora/errors";import{cloudflareEmailProvider as l}from"@visulima/email/providers/cloudflare-email";import{o as d,c as f,f as u}from"./provider-transport-BZngzyeS.mjs";const g=o=>{const n=l({send:o.send});return{send:async e=>{await n.initialize();const i=e.cc!==void 0&&e.cc.length>0,a=e.bcc!==void 0&&e.bcc.length>0;if(i||a)throw new t("INTERNAL","@lunora/mail: Cloudflare Email Workers does not support cc/bcc — fan out one send per recipient instead");const{first:s,list:r}=d(e.to);if(r.length>1)throw new t("INTERNAL",`@lunora/mail: Cloudflare Email Workers is single-recipient but received ${String(r.length)} \`to\` addresses — fan out one send per recipient instead`);const c=await n.sendEmail(f(e,o.from,s));return u(c)}}};export{g as createCloudflareTransport};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as c}from"@lunora/errors";import{u as d,d as u}from"./shard-CcKsuWAm.mjs";const h="message could not be processed",m=(t,a)=>{console.error("@lunora/mail/inbound: dropping message —",t),a.message.setReject(h)},y=t=>{const a=t.onError??m;return async(e,n,s)=>{const r={ctx:s,env:n,message:e};try{const o=await t.parse(e.raw);if(t.verify&&await t.verify(o,r)===!1)throw new c("INTERNAL","@lunora/mail/inbound: sender verification rejected the message");await t.dispatch(o,r)}catch(o){await a(o,r)}}},i=32768,l=t=>{let a="";for(let e=0;e<t.length;e+=i)a+=String.fromCharCode(...t.subarray(e,e+i));return btoa(a)},f=t=>t.attachments.length===0?t:{...t,attachments:t.attachments.map(a=>{const{content:e}=a;if(typeof e=="string")return a;const n=e instanceof Uint8Array?e:new Uint8Array(e);return{...a,content:l(n),encoding:"base64"}})},N=t=>{const a=t.shardKey??u,e=t.resolveArgs??(n=>f(n));return async(n,s)=>{const r=t.adminToken??(typeof s.env.LUNORA_ADMIN_TOKEN=="string"?s.env.LUNORA_ADMIN_TOKEN:void 0);if(r===void 0||r==="")throw new c("INTERNAL","@lunora/mail/inbound: missing LUNORA_ADMIN_TOKEN — cannot authorize inbound dispatch to the shard RPC.");const o={args:e(n,s),functionPath:t.functionPath,shardKey:a};await d(t.shard,{adminToken:r,envelope:o,jurisdiction:t.jurisdiction,label:`@lunora/mail/inbound: dispatch to \`${t.functionPath}\``,shardKey:a})}};export{y as createInboundEmailHandler,N as dispatchToLunoraFunction};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as c}from"@lunora/errors";import{m as s,f}from"./address-Y4Z--6ZO.mjs";import{isCaptureTransport as d}from"./createCaptureTransport-BPdMkU0O.mjs";import{createCloudflareTransport as l}from"./createCloudflareTransport-V23CVw7F.mjs";import{toQueuedPayload as p}from"./consumeQueuedSend-BXrPhND5.mjs";import h from"./renderEmail-DM8_pWKa.mjs";import w from"./createResendTransport-CpSJ4j7C.mjs";const y=e=>{if(e.cloudflareSend)return l({from:e.from,send:e.cloudflareSend});if(e.apiKey)return w(e.apiKey,e.from);throw new c("INTERNAL","@lunora/mail: a transport is required — pass `transport`, `cloudflareSend` (Cloudflare Email Workers, the default), or `apiKey` (Resend)")},L=e=>{if(!e.from)throw new c("INTERNAL","@lunora/mail: `from` is required");const u=e.transport??y(e),n=async r=>{let{html:t}=r,{text:a}=r;if(r.react){const o=await h(r.react);t=t??o.html,a=a??o.text}if(s("subject",r.subject),r.headers)for(const[o,m]of Object.entries(r.headers))s(`header name "${o}"`,o),s(`header "${o}" value`,m);const i=r.from??e.from;return f({bcc:r.bcc,cc:r.cc,from:i,replyTo:r.replyTo,to:r.to}),{bcc:r.bcc,cc:r.cc,from:i,headers:r.headers,html:t,replyTo:r.replyTo,subject:r.subject,text:a,to:r.to}};return{queue:async r=>{if(!e.queue){if(d(u)){const a=await n(r);return await u.send(a),{queued:!0}}throw new c("INTERNAL","@lunora/mail: `queue` binding is required for mailer.queue()")}const t=await n(r);return await e.queue.send(p(t)),{queued:!0}},send:async r=>{const t=await n(r);return u.send(t)}}};export{L as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{resendProvider as a}from"@visulima/email/providers/resend";import{o as m,c,f as d}from"./provider-transport-BZngzyeS.mjs";const p=(o,e)=>{const t=a({apiKey:o});return{send:async r=>{await t.initialize();const{first:n,list:i}=m(r.to),s=await t.sendEmail(c(r,e,i.length===1?n:i));return d(s)}}};export{p as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import l from"postal-mime";import{m as i}from"./address-Y4Z--6ZO.mjs";const s=(n,e)=>{if(e!==void 0)return i(`inbound ${n}`,e),e},m=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??"",a=(n,e)=>new RegExp(String.raw`\b${e}=([a-zA-Z]+)`,"i").exec(n)?.[1]?.toLowerCase()??null,p=n=>n===void 0||n===""?{dkim:null,dmarc:null,spf:null}:{dkim:a(n,"dkim"),dmarc:a(n,"dmarc"),spf:a(n,"spf")},g=async n=>{const e=await l.parse(n),d={};for(const o of e.headers)i(`inbound header \`${o.key}\``,o.value),d[o.key]=o.value;const c=e.headers.find(o=>o.key==="authentication-results")?.value,u=(e.to??[]).map(o=>{const r=m(o);return i("inbound to",r),r}),t=e.from?m(e.from):"";return i("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:p(c),from:t,headers:d,...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:u}};export{g as parseInboundEmail};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as s}from"@lunora/errors";import{i,o as n}from"./address-Y4Z--6ZO.mjs";const e=r=>r instanceof Error?r.message:r==null?"send failed":typeof r=="string"?r:typeof r=="number"||typeof r=="boolean"||typeof r=="bigint"?r.toString():JSON.stringify(r)??"send failed",c=r=>{const o=i(r),[t]=o??[];if(!o||t===void 0)throw new s("INTERNAL","@lunora/mail: at least one recipient is required");return{first:t,list:o}},f=(r,o,t)=>({bcc:i(r.bcc),cc:i(r.cc),from:n(r.from??o),headers:r.headers,html:r.html,replyTo:r.replyTo?n(r.replyTo):void 0,subject:r.subject,text:r.text,to:t}),d=r=>{if(!r.success||!r.data)throw console.error(`@lunora/mail: send failed: ${e(r.error)}`),new s("INTERNAL","@lunora/mail: send failed");return{id:r.data.messageId}};export{f as c,d as f,c as o};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{render as e}from"@react-email/render";const n=async t=>{const[r,a]=await Promise.all([e(t,{pretty:!1}),e(t,{plainText:!0})]);return{html:r,text:a}};export{n as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as a}from"@lunora/errors";const c="__root__",s=(o,r)=>{if(r===void 0)return o;if(typeof o.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 o.jurisdiction(r)},d=async(o,r)=>{const i=s(o,r.jurisdiction),n=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(n.ok===!1)throw new a("INTERNAL",`${r.label} failed (HTTP ${String(n.status??"?")}).`);const t=await n.json();if(typeof t=="object"&&t!==null&&"error"in t){const{error:e}=t;if(e!=null)throw new a("INTERNAL",`${r.label} returned an error: ${JSON.stringify(e)}`)}return t};export{c as d,d as u};
|
package/dist/testing.mjs
CHANGED
|
@@ -1,65 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
const GET_CAPTURED_MAIL_OP = "__lunora_admin__:getCapturedMail";
|
|
4
|
-
const DEFAULT_RPC_PATH = "/_lunora/rpc";
|
|
5
|
-
const TRAILING_SLASH = /\/$/;
|
|
6
|
-
const sleep = async (ms) => new Promise((resolve) => {
|
|
7
|
-
setTimeout(resolve, ms);
|
|
8
|
-
});
|
|
9
|
-
const recipients = (mail) => Array.isArray(mail.to) ? mail.to : [mail.to];
|
|
10
|
-
const listCapturedMail = async (options) => {
|
|
11
|
-
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
12
|
-
const endpoint = `${options.baseUrl.replace(TRAILING_SLASH, "")}${DEFAULT_RPC_PATH}`;
|
|
13
|
-
const response = await fetchImpl(endpoint, {
|
|
14
|
-
body: JSON.stringify({ args: { limit: options.limit ?? 50 }, functionPath: GET_CAPTURED_MAIL_OP }),
|
|
15
|
-
headers: { authorization: `Bearer ${options.adminToken}`, "content-type": "application/json" },
|
|
16
|
-
method: "POST"
|
|
17
|
-
});
|
|
18
|
-
if (!response.ok) {
|
|
19
|
-
throw new LunoraError("INTERNAL", `@lunora/mail/testing: getCapturedMail failed (HTTP ${String(response.status)})`);
|
|
20
|
-
}
|
|
21
|
-
const body = await response.json();
|
|
22
|
-
return body.result?.entries ?? [];
|
|
23
|
-
};
|
|
24
|
-
const waitForMail = async (options) => {
|
|
25
|
-
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
26
|
-
const pollMs = options.pollMs ?? 250;
|
|
27
|
-
const deadline = Date.now() + timeoutMs;
|
|
28
|
-
for (; ; ) {
|
|
29
|
-
const entries = await listCapturedMail(options);
|
|
30
|
-
const match = entries.find(
|
|
31
|
-
(mail) => recipients(mail).includes(options.to) && (options.subjectMatch === void 0 || mail.subject.includes(options.subjectMatch))
|
|
32
|
-
);
|
|
33
|
-
if (match) {
|
|
34
|
-
return match;
|
|
35
|
-
}
|
|
36
|
-
if (Date.now() >= deadline) {
|
|
37
|
-
throw new LunoraError(
|
|
38
|
-
"INTERNAL",
|
|
39
|
-
`@lunora/mail/testing: no mail to "${options.to}"${options.subjectMatch === void 0 ? "" : ` matching "${options.subjectMatch}"`} within ${String(timeoutMs)}ms`
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
await sleep(pollMs);
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
const URL_PATTERN = /https?:\/\/[^\s"'<>)]+/g;
|
|
46
|
-
const AMPERSAND_ENTITY = /&(?:amp|#0*38|#x0*26);/giu;
|
|
47
|
-
const decodeUrlEntities = (url) => url.replaceAll(AMPERSAND_ENTITY, "&");
|
|
48
|
-
const extractLink = (mail, options = {}) => {
|
|
49
|
-
for (const source of [mail.html, mail.text]) {
|
|
50
|
-
if (source === void 0) {
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
const matches = source.match(URL_PATTERN) ?? [];
|
|
54
|
-
const link = matches.find((candidate) => options.match === void 0 || candidate.includes(options.match));
|
|
55
|
-
if (link !== void 0) {
|
|
56
|
-
return decodeUrlEntities(link);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
throw new LunoraError(
|
|
60
|
-
"INTERNAL",
|
|
61
|
-
`@lunora/mail/testing: no link${options.match === void 0 ? "" : ` containing "${options.match}"`} found in the captured message`
|
|
62
|
-
);
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export { extractLink, listCapturedMail, waitForMail };
|
|
1
|
+
import{LunoraError as e}from"@lunora/errors";const s="__lunora_admin__:getCapturedMail",c="/_lunora/rpc",l=/\/$/,u=async t=>new Promise(i=>{setTimeout(i,t)}),h=t=>Array.isArray(t.to)?t.to:[t.to],m=async t=>{const i=t.fetch??globalThis.fetch,n=`${t.baseUrl.replace(l,"")}${c}`,a=await i(n,{body:JSON.stringify({args:{limit:t.limit??50},functionPath:s}),headers:{authorization:`Bearer ${t.adminToken}`,"content-type":"application/json"},method:"POST"});if(!a.ok)throw new e("INTERNAL",`@lunora/mail/testing: getCapturedMail failed (HTTP ${String(a.status)})`);return(await a.json()).result?.entries??[]},w=async t=>{const i=t.timeoutMs??1e4,n=t.pollMs??250,a=Date.now()+i;for(;;){const o=(await m(t)).find(r=>h(r).includes(t.to)&&(t.subjectMatch===void 0||r.subject.includes(t.subjectMatch)));if(o)return o;if(Date.now()>=a)throw new e("INTERNAL",`@lunora/mail/testing: no mail to "${t.to}"${t.subjectMatch===void 0?"":` matching "${t.subjectMatch}"`} within ${String(i)}ms`);await u(n)}},d=/https?:\/\/[^\s"'<>)]+/g,f=/&(?:amp|#0*38|#x0*26);/giu,g=t=>t.replaceAll(f,"&"),M=(t,i={})=>{for(const n of[t.html,t.text]){if(n===void 0)continue;const a=(n.match(d)??[]).find(o=>i.match===void 0||o.includes(i.match));if(a!==void 0)return g(a)}throw new e("INTERNAL",`@lunora/mail/testing: no link${i.match===void 0?"":` containing "${i.match}"`} found in the captured message`)};export{M as extractLink,m as listCapturedMail,w as waitForMail};
|
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.19",
|
|
4
4
|
"description": "Email for Lunora: Resend adapter, TSX templates, and queue-backed sends",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
"access": "public"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
57
|
+
"@lunora/errors": "1.0.0-alpha.8",
|
|
58
58
|
"@react-email/render": "2.1.0",
|
|
59
|
-
"@visulima/email": "2.1.
|
|
59
|
+
"@visulima/email": "2.1.2",
|
|
60
60
|
"postal-mime": "2.7.5"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
|
|
3
|
-
const MAX_EMAIL_LENGTH = 320;
|
|
4
|
-
const MAX_NAME_LENGTH = 256;
|
|
5
|
-
const ADDRESS_PATTERN = /^([^<]*)<([^>]*)>\s*$/;
|
|
6
|
-
const assertSafeAddressField = (field, value) => {
|
|
7
|
-
if (value.includes("\r") || value.includes("\n") || value.includes(",")) {
|
|
8
|
-
throw new LunoraError("INTERNAL", `@lunora/mail: address ${field} must not contain CR, LF, or comma`);
|
|
9
|
-
}
|
|
10
|
-
};
|
|
11
|
-
const assertSafeHeaderValue = (label, value) => {
|
|
12
|
-
if (value.includes("\r") || value.includes("\n")) {
|
|
13
|
-
throw new LunoraError("INTERNAL", `@lunora/mail: ${label} must not contain CR or LF`);
|
|
14
|
-
}
|
|
15
|
-
};
|
|
16
|
-
const toBracketedAddress = (name, email) => {
|
|
17
|
-
if (name.length > MAX_NAME_LENGTH) {
|
|
18
|
-
throw new LunoraError("INTERNAL", `@lunora/mail: address name must be <= ${String(MAX_NAME_LENGTH)} characters`);
|
|
19
|
-
}
|
|
20
|
-
if (email.length > MAX_EMAIL_LENGTH) {
|
|
21
|
-
throw new LunoraError("INTERNAL", `@lunora/mail: address email must be <= ${String(MAX_EMAIL_LENGTH)} characters`);
|
|
22
|
-
}
|
|
23
|
-
if (name) {
|
|
24
|
-
assertSafeAddressField("name", name);
|
|
25
|
-
}
|
|
26
|
-
assertSafeAddressField("email", email);
|
|
27
|
-
return name ? { email, name } : { email };
|
|
28
|
-
};
|
|
29
|
-
const toBareAddress = (input) => {
|
|
30
|
-
const email = input.trim();
|
|
31
|
-
if (email.length > MAX_EMAIL_LENGTH) {
|
|
32
|
-
throw new LunoraError("INTERNAL", `@lunora/mail: address email must be <= ${String(MAX_EMAIL_LENGTH)} characters`);
|
|
33
|
-
}
|
|
34
|
-
assertSafeAddressField("email", email);
|
|
35
|
-
return { email };
|
|
36
|
-
};
|
|
37
|
-
const toAddress = (input) => {
|
|
38
|
-
const match = ADDRESS_PATTERN.exec(input);
|
|
39
|
-
const email = (match?.[2] ?? "").trim();
|
|
40
|
-
if (match && email) {
|
|
41
|
-
return toBracketedAddress((match[1] ?? "").trim(), email);
|
|
42
|
-
}
|
|
43
|
-
return toBareAddress(input);
|
|
44
|
-
};
|
|
45
|
-
const toAddressList = (input) => {
|
|
46
|
-
if (input === void 0) {
|
|
47
|
-
return void 0;
|
|
48
|
-
}
|
|
49
|
-
const list = Array.isArray(input) ? input : [input];
|
|
50
|
-
return list.map((entry) => toAddress(entry));
|
|
51
|
-
};
|
|
52
|
-
const assertSafeAddresses = (payload) => {
|
|
53
|
-
toAddressList(payload.to);
|
|
54
|
-
toAddressList(payload.cc);
|
|
55
|
-
toAddressList(payload.bcc);
|
|
56
|
-
if (payload.from !== void 0) {
|
|
57
|
-
toAddress(payload.from);
|
|
58
|
-
}
|
|
59
|
-
if (payload.replyTo !== void 0) {
|
|
60
|
-
toAddress(payload.replyTo);
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export { assertSafeHeaderValue as a, assertSafeAddresses as b, toAddress as c, toAddressList as t };
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
|
|
3
|
-
const toQueuedPayload = (options) => {
|
|
4
|
-
return {
|
|
5
|
-
bcc: options.bcc,
|
|
6
|
-
cc: options.cc,
|
|
7
|
-
from: options.from,
|
|
8
|
-
headers: options.headers,
|
|
9
|
-
html: options.html,
|
|
10
|
-
replyTo: options.replyTo,
|
|
11
|
-
subject: options.subject,
|
|
12
|
-
text: options.text,
|
|
13
|
-
to: options.to
|
|
14
|
-
};
|
|
15
|
-
};
|
|
16
|
-
const consumeQueuedSend = async (mailer, payload) => {
|
|
17
|
-
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
18
|
-
throw new LunoraError("INTERNAL", "@lunora/mail: queue message body must be an object");
|
|
19
|
-
}
|
|
20
|
-
const candidate = payload;
|
|
21
|
-
if (typeof candidate.subject !== "string") {
|
|
22
|
-
throw new TypeError("@lunora/mail: queue message must have a string `subject`");
|
|
23
|
-
}
|
|
24
|
-
const recipientIsString = typeof candidate.to === "string";
|
|
25
|
-
const recipientIsStringArray = Array.isArray(candidate.to) && candidate.to.every((value) => typeof value === "string");
|
|
26
|
-
if (!recipientIsString && !recipientIsStringArray) {
|
|
27
|
-
throw new LunoraError("INTERNAL", "@lunora/mail: queue message `to` must be a string or string[]");
|
|
28
|
-
}
|
|
29
|
-
const assertOptionalString = (field, value) => {
|
|
30
|
-
if (value === void 0) {
|
|
31
|
-
return void 0;
|
|
32
|
-
}
|
|
33
|
-
if (typeof value !== "string") {
|
|
34
|
-
throw new TypeError(`@lunora/mail: queue message \`${field}\` must be a string`);
|
|
35
|
-
}
|
|
36
|
-
return value;
|
|
37
|
-
};
|
|
38
|
-
const assertOptionalStringList = (field, value) => {
|
|
39
|
-
if (value === void 0) {
|
|
40
|
-
return void 0;
|
|
41
|
-
}
|
|
42
|
-
if (typeof value === "string") {
|
|
43
|
-
return [value];
|
|
44
|
-
}
|
|
45
|
-
if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
|
|
46
|
-
return value;
|
|
47
|
-
}
|
|
48
|
-
throw new TypeError(`@lunora/mail: queue message \`${field}\` must be a string or string[]`);
|
|
49
|
-
};
|
|
50
|
-
let headers;
|
|
51
|
-
if (candidate.headers !== void 0) {
|
|
52
|
-
if (!candidate.headers || typeof candidate.headers !== "object" || Array.isArray(candidate.headers)) {
|
|
53
|
-
throw new TypeError("@lunora/mail: queue message `headers` must be an object of string values");
|
|
54
|
-
}
|
|
55
|
-
const entries = Object.entries(candidate.headers);
|
|
56
|
-
for (const [key, value] of entries) {
|
|
57
|
-
if (typeof value !== "string") {
|
|
58
|
-
throw new TypeError(`@lunora/mail: queue message header "${key}" must be a string`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
headers = candidate.headers;
|
|
62
|
-
}
|
|
63
|
-
const options = {
|
|
64
|
-
bcc: assertOptionalStringList("bcc", candidate.bcc),
|
|
65
|
-
cc: assertOptionalStringList("cc", candidate.cc),
|
|
66
|
-
from: assertOptionalString("from", candidate.from),
|
|
67
|
-
headers,
|
|
68
|
-
html: assertOptionalString("html", candidate.html),
|
|
69
|
-
replyTo: assertOptionalString("replyTo", candidate.replyTo),
|
|
70
|
-
subject: candidate.subject,
|
|
71
|
-
text: assertOptionalString("text", candidate.text),
|
|
72
|
-
to: candidate.to
|
|
73
|
-
};
|
|
74
|
-
return mailer.send(options);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export { consumeQueuedSend, toQueuedPayload };
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
import { createCaptureTransport } from './createCaptureTransport-Crz_8822.mjs';
|
|
3
|
-
import createMailer from './createMailer-B4Z_Cgiu.mjs';
|
|
4
|
-
import { D as DEFAULT_ROOT_SHARD, p as postShardRpc } from './shard-DEDcQoFg.mjs';
|
|
5
|
-
|
|
6
|
-
const RECORD_MAIL_OP = "__lunora_admin__:recordMail";
|
|
7
|
-
const DEV_ENVIRONMENT_PATTERN = /^(?:dev(?:elopment)?|local(?:host)?|test)$/iu;
|
|
8
|
-
const ENVIRONMENT_VARS = ["CF_ENV", "ENVIRONMENT", "NODE_ENV", "WORKER_ENV"];
|
|
9
|
-
const requireStringEnv = (env, name) => {
|
|
10
|
-
const value = env[name];
|
|
11
|
-
if (typeof value !== "string" || value === "") {
|
|
12
|
-
throw new LunoraError("INTERNAL", `@lunora/mail: missing env var \`${name}\` — set it in .dev.vars (and \`wrangler secret put ${name}\` for secrets).`);
|
|
13
|
-
}
|
|
14
|
-
return value;
|
|
15
|
-
};
|
|
16
|
-
const shouldCaptureMail = (env) => {
|
|
17
|
-
const flag = env["LUNORA_MAIL_CAPTURE"];
|
|
18
|
-
if (typeof flag === "string") {
|
|
19
|
-
const normalized = flag.toLowerCase();
|
|
20
|
-
if (normalized === "1" || normalized === "true") {
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
if (normalized === "0" || normalized === "false") {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
console.warn(
|
|
27
|
-
`@lunora/mail: unrecognized LUNORA_MAIL_CAPTURE value "${flag}" — expected "1"/"true" or "0"/"false"; falling back to environment detection.`
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
return ENVIRONMENT_VARS.some((key) => {
|
|
31
|
-
const value = env[key];
|
|
32
|
-
return typeof value === "string" && DEV_ENVIRONMENT_PATTERN.test(value);
|
|
33
|
-
});
|
|
34
|
-
};
|
|
35
|
-
const createCaptureSink = (env, rootShard = DEFAULT_ROOT_SHARD, jurisdiction) => {
|
|
36
|
-
return {
|
|
37
|
-
record: async (mail) => {
|
|
38
|
-
const binding = env["SHARD"];
|
|
39
|
-
const adminToken = typeof env["LUNORA_ADMIN_TOKEN"] === "string" ? env["LUNORA_ADMIN_TOKEN"] : void 0;
|
|
40
|
-
if (binding === void 0 || adminToken === void 0) {
|
|
41
|
-
return { id: "uncaptured" };
|
|
42
|
-
}
|
|
43
|
-
try {
|
|
44
|
-
const body = await postShardRpc(binding, {
|
|
45
|
-
adminToken,
|
|
46
|
-
envelope: { args: mail, functionPath: RECORD_MAIL_OP },
|
|
47
|
-
jurisdiction,
|
|
48
|
-
label: "@lunora/mail: recording captured mail",
|
|
49
|
-
shardKey: rootShard
|
|
50
|
-
});
|
|
51
|
-
return { id: body.result?.id ?? "captured" };
|
|
52
|
-
} catch (error) {
|
|
53
|
-
console.error("@lunora/mail: failed to record captured mail into the studio inbox —", error);
|
|
54
|
-
return { id: "uncaptured" };
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
};
|
|
59
|
-
const createMailerFromEnv = (env, options = {}) => {
|
|
60
|
-
const from = requireStringEnv(env, "MAIL_FROM");
|
|
61
|
-
if (shouldCaptureMail(env)) {
|
|
62
|
-
return createMailer({ from, transport: createCaptureTransport(createCaptureSink(env, options.rootShard, options.jurisdiction)) });
|
|
63
|
-
}
|
|
64
|
-
if (options.cloudflareSend) {
|
|
65
|
-
return createMailer({ cloudflareSend: options.cloudflareSend, from });
|
|
66
|
-
}
|
|
67
|
-
const apiKey = typeof env["RESEND_API_KEY"] === "string" ? env["RESEND_API_KEY"] : void 0;
|
|
68
|
-
if (apiKey !== void 0 && apiKey !== "") {
|
|
69
|
-
return createMailer({ apiKey, from });
|
|
70
|
-
}
|
|
71
|
-
throw new LunoraError(
|
|
72
|
-
"INTERNAL",
|
|
73
|
-
"@lunora/mail: no transport configured — provide `cloudflareSend` (a SEND_EMAIL binding) or RESEND_API_KEY, or run in a dev environment to capture."
|
|
74
|
-
);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export { createCaptureSink, createMailerFromEnv, shouldCaptureMail };
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
const CAPTURE_TRANSPORT_BRAND = /* @__PURE__ */ Symbol.for("@lunora/mail.captureTransport");
|
|
2
|
-
const isCaptureTransport = (transport) => transport[CAPTURE_TRANSPORT_BRAND] === true;
|
|
3
|
-
const createCaptureTransport = (sink) => {
|
|
4
|
-
const transport = {
|
|
5
|
-
[CAPTURE_TRANSPORT_BRAND]: true,
|
|
6
|
-
send: async (payload) => sink.record(payload)
|
|
7
|
-
};
|
|
8
|
-
return transport;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export { CAPTURE_TRANSPORT_BRAND, createCaptureTransport, isCaptureTransport };
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
import { cloudflareEmailProvider } from '@visulima/email/providers/cloudflare-email';
|
|
3
|
-
import { r as requireRecipients, t as toProviderEmail, i as interpretSendResult } from './provider-transport-C0xHi3oy.mjs';
|
|
4
|
-
|
|
5
|
-
const createCloudflareTransport = (options) => {
|
|
6
|
-
const provider = cloudflareEmailProvider({ send: options.send });
|
|
7
|
-
return {
|
|
8
|
-
send: async (payload) => {
|
|
9
|
-
await provider.initialize();
|
|
10
|
-
const hasCc = payload.cc !== void 0 && payload.cc.length > 0;
|
|
11
|
-
const hasBcc = payload.bcc !== void 0 && payload.bcc.length > 0;
|
|
12
|
-
if (hasCc || hasBcc) {
|
|
13
|
-
throw new LunoraError("INTERNAL", "@lunora/mail: Cloudflare Email Workers does not support cc/bcc — fan out one send per recipient instead");
|
|
14
|
-
}
|
|
15
|
-
const { first, list } = requireRecipients(payload.to);
|
|
16
|
-
if (list.length > 1) {
|
|
17
|
-
throw new LunoraError(
|
|
18
|
-
"INTERNAL",
|
|
19
|
-
`@lunora/mail: Cloudflare Email Workers is single-recipient but received ${String(list.length)} \`to\` addresses — fan out one send per recipient instead`
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
const result = await provider.sendEmail(toProviderEmail(payload, options.from, first));
|
|
23
|
-
return interpretSendResult(result);
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export { createCloudflareTransport };
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
import { p as postShardRpc, D as DEFAULT_ROOT_SHARD } from './shard-DEDcQoFg.mjs';
|
|
3
|
-
|
|
4
|
-
const GENERIC_REJECT_REASON = "message could not be processed";
|
|
5
|
-
const rejectOnError = (error, context) => {
|
|
6
|
-
console.error("@lunora/mail/inbound: dropping message —", error);
|
|
7
|
-
context.message.setReject(GENERIC_REJECT_REASON);
|
|
8
|
-
};
|
|
9
|
-
const createInboundEmailHandler = (options) => {
|
|
10
|
-
const onError = options.onError ?? rejectOnError;
|
|
11
|
-
return async (message, env, context_) => {
|
|
12
|
-
const context = { ctx: context_, env, message };
|
|
13
|
-
try {
|
|
14
|
-
const parsed = await options.parse(message.raw);
|
|
15
|
-
if (options.verify) {
|
|
16
|
-
const verified = await options.verify(parsed, context);
|
|
17
|
-
if (verified === false) {
|
|
18
|
-
throw new LunoraError("INTERNAL", "@lunora/mail/inbound: sender verification rejected the message");
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
await options.dispatch(parsed, context);
|
|
22
|
-
} catch (error) {
|
|
23
|
-
await onError(error, context);
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
};
|
|
27
|
-
const BASE64_CHUNK = 32768;
|
|
28
|
-
const toBase64 = (bytes) => {
|
|
29
|
-
let binary = "";
|
|
30
|
-
for (let index = 0; index < bytes.length; index += BASE64_CHUNK) {
|
|
31
|
-
binary += String.fromCharCode(...bytes.subarray(index, index + BASE64_CHUNK));
|
|
32
|
-
}
|
|
33
|
-
return btoa(binary);
|
|
34
|
-
};
|
|
35
|
-
const toJsonSafeEmail = (email) => {
|
|
36
|
-
if (email.attachments.length === 0) {
|
|
37
|
-
return email;
|
|
38
|
-
}
|
|
39
|
-
return {
|
|
40
|
-
...email,
|
|
41
|
-
attachments: email.attachments.map((attachment) => {
|
|
42
|
-
const { content } = attachment;
|
|
43
|
-
if (typeof content === "string") {
|
|
44
|
-
return attachment;
|
|
45
|
-
}
|
|
46
|
-
const bytes = content instanceof Uint8Array ? content : new Uint8Array(content);
|
|
47
|
-
return { ...attachment, content: toBase64(bytes), encoding: "base64" };
|
|
48
|
-
})
|
|
49
|
-
};
|
|
50
|
-
};
|
|
51
|
-
const dispatchToLunoraFunction = (options) => {
|
|
52
|
-
const shardKey = options.shardKey ?? DEFAULT_ROOT_SHARD;
|
|
53
|
-
const resolveArgs = options.resolveArgs ?? ((email) => toJsonSafeEmail(email));
|
|
54
|
-
return async (email, context) => {
|
|
55
|
-
const adminToken = options.adminToken ?? (typeof context.env["LUNORA_ADMIN_TOKEN"] === "string" ? context.env["LUNORA_ADMIN_TOKEN"] : void 0);
|
|
56
|
-
if (adminToken === void 0 || adminToken === "") {
|
|
57
|
-
throw new LunoraError("INTERNAL", "@lunora/mail/inbound: missing LUNORA_ADMIN_TOKEN — cannot authorize inbound dispatch to the shard RPC.");
|
|
58
|
-
}
|
|
59
|
-
const envelope = {
|
|
60
|
-
args: resolveArgs(email, context),
|
|
61
|
-
functionPath: options.functionPath,
|
|
62
|
-
shardKey
|
|
63
|
-
};
|
|
64
|
-
await postShardRpc(options.shard, {
|
|
65
|
-
adminToken,
|
|
66
|
-
envelope,
|
|
67
|
-
jurisdiction: options.jurisdiction,
|
|
68
|
-
label: `@lunora/mail/inbound: dispatch to \`${options.functionPath}\``,
|
|
69
|
-
shardKey
|
|
70
|
-
});
|
|
71
|
-
};
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
export { createInboundEmailHandler, dispatchToLunoraFunction };
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
import { a as assertSafeHeaderValue, b as assertSafeAddresses } from './address-vSUAVU2T.mjs';
|
|
3
|
-
import { isCaptureTransport } from './createCaptureTransport-Crz_8822.mjs';
|
|
4
|
-
import { createCloudflareTransport } from './createCloudflareTransport-DXh1nUNi.mjs';
|
|
5
|
-
import { toQueuedPayload } from './consumeQueuedSend-B9hTDOZ6.mjs';
|
|
6
|
-
import renderEmail from './renderEmail-hyS1bpVP.mjs';
|
|
7
|
-
import createResendTransport from './createResendTransport-CMQWjZoi.mjs';
|
|
8
|
-
|
|
9
|
-
const buildDefaultTransport = (options) => {
|
|
10
|
-
if (options.cloudflareSend) {
|
|
11
|
-
return createCloudflareTransport({ from: options.from, send: options.cloudflareSend });
|
|
12
|
-
}
|
|
13
|
-
if (options.apiKey) {
|
|
14
|
-
return createResendTransport(options.apiKey, options.from);
|
|
15
|
-
}
|
|
16
|
-
throw new LunoraError(
|
|
17
|
-
"INTERNAL",
|
|
18
|
-
"@lunora/mail: a transport is required — pass `transport`, `cloudflareSend` (Cloudflare Email Workers, the default), or `apiKey` (Resend)"
|
|
19
|
-
);
|
|
20
|
-
};
|
|
21
|
-
const createMailer = (options) => {
|
|
22
|
-
if (!options.from) {
|
|
23
|
-
throw new LunoraError("INTERNAL", "@lunora/mail: `from` is required");
|
|
24
|
-
}
|
|
25
|
-
const transport = options.transport ?? buildDefaultTransport(options);
|
|
26
|
-
const buildPayload = async (options_) => {
|
|
27
|
-
let { html } = options_;
|
|
28
|
-
let { text } = options_;
|
|
29
|
-
if (options_.react) {
|
|
30
|
-
const rendered = await renderEmail(options_.react);
|
|
31
|
-
html = html ?? rendered.html;
|
|
32
|
-
text = text ?? rendered.text;
|
|
33
|
-
}
|
|
34
|
-
assertSafeHeaderValue("subject", options_.subject);
|
|
35
|
-
if (options_.headers) {
|
|
36
|
-
for (const [name, value] of Object.entries(options_.headers)) {
|
|
37
|
-
assertSafeHeaderValue(`header name "${name}"`, name);
|
|
38
|
-
assertSafeHeaderValue(`header "${name}" value`, value);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
const from = options_.from ?? options.from;
|
|
42
|
-
assertSafeAddresses({
|
|
43
|
-
bcc: options_.bcc,
|
|
44
|
-
cc: options_.cc,
|
|
45
|
-
from,
|
|
46
|
-
replyTo: options_.replyTo,
|
|
47
|
-
to: options_.to
|
|
48
|
-
});
|
|
49
|
-
return {
|
|
50
|
-
bcc: options_.bcc,
|
|
51
|
-
cc: options_.cc,
|
|
52
|
-
from,
|
|
53
|
-
headers: options_.headers,
|
|
54
|
-
html,
|
|
55
|
-
replyTo: options_.replyTo,
|
|
56
|
-
subject: options_.subject,
|
|
57
|
-
text,
|
|
58
|
-
to: options_.to
|
|
59
|
-
};
|
|
60
|
-
};
|
|
61
|
-
const send = async (options_) => {
|
|
62
|
-
const payload = await buildPayload(options_);
|
|
63
|
-
return transport.send(payload);
|
|
64
|
-
};
|
|
65
|
-
const queue = async (options_) => {
|
|
66
|
-
if (!options.queue) {
|
|
67
|
-
if (isCaptureTransport(transport)) {
|
|
68
|
-
const captured = await buildPayload(options_);
|
|
69
|
-
await transport.send(captured);
|
|
70
|
-
return { queued: true };
|
|
71
|
-
}
|
|
72
|
-
throw new LunoraError("INTERNAL", "@lunora/mail: `queue` binding is required for mailer.queue()");
|
|
73
|
-
}
|
|
74
|
-
const payload = await buildPayload(options_);
|
|
75
|
-
await options.queue.send(toQueuedPayload(payload));
|
|
76
|
-
return { queued: true };
|
|
77
|
-
};
|
|
78
|
-
return { queue, send };
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
export { createMailer as default };
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { resendProvider } from '@visulima/email/providers/resend';
|
|
2
|
-
import { r as requireRecipients, t as toProviderEmail, i as interpretSendResult } from './provider-transport-C0xHi3oy.mjs';
|
|
3
|
-
|
|
4
|
-
const createResendTransport = (apiKey, defaultFrom) => {
|
|
5
|
-
const provider = resendProvider({ apiKey });
|
|
6
|
-
return {
|
|
7
|
-
send: async (payload) => {
|
|
8
|
-
await provider.initialize();
|
|
9
|
-
const { first, list } = requireRecipients(payload.to);
|
|
10
|
-
const result = await provider.sendEmail(toProviderEmail(payload, defaultFrom, list.length === 1 ? first : list));
|
|
11
|
-
return interpretSendResult(result);
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export { createResendTransport as default };
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import PostalMime from 'postal-mime';
|
|
2
|
-
import { a as assertSafeHeaderValue } from './address-vSUAVU2T.mjs';
|
|
3
|
-
|
|
4
|
-
const safe = (label, value) => {
|
|
5
|
-
if (value === void 0) {
|
|
6
|
-
return void 0;
|
|
7
|
-
}
|
|
8
|
-
assertSafeHeaderValue(`inbound ${label}`, value);
|
|
9
|
-
return value;
|
|
10
|
-
};
|
|
11
|
-
const formatAddress = (entry) => {
|
|
12
|
-
if (entry.address !== void 0 && entry.address !== "") {
|
|
13
|
-
return entry.name ? `${entry.name} <${entry.address}>` : entry.address;
|
|
14
|
-
}
|
|
15
|
-
if (entry.group) {
|
|
16
|
-
return entry.group.map((member) => member.address ?? "").filter((address) => address !== "").join(", ");
|
|
17
|
-
}
|
|
18
|
-
return entry.name ?? "";
|
|
19
|
-
};
|
|
20
|
-
const authVerdict = (authResults, method) => {
|
|
21
|
-
const match = new RegExp(String.raw`\b${method}=([a-zA-Z]+)`, "i").exec(authResults);
|
|
22
|
-
return match?.[1]?.toLowerCase() ?? null;
|
|
23
|
-
};
|
|
24
|
-
const parseAuthentication = (authResults) => {
|
|
25
|
-
if (authResults === void 0 || authResults === "") {
|
|
26
|
-
return { dkim: null, dmarc: null, spf: null };
|
|
27
|
-
}
|
|
28
|
-
return {
|
|
29
|
-
dkim: authVerdict(authResults, "dkim"),
|
|
30
|
-
dmarc: authVerdict(authResults, "dmarc"),
|
|
31
|
-
spf: authVerdict(authResults, "spf")
|
|
32
|
-
};
|
|
33
|
-
};
|
|
34
|
-
const parseInboundEmail = async (raw) => {
|
|
35
|
-
const parsed = await PostalMime.parse(raw);
|
|
36
|
-
const headers = {};
|
|
37
|
-
for (const header of parsed.headers) {
|
|
38
|
-
assertSafeHeaderValue(`inbound header \`${header.key}\``, header.value);
|
|
39
|
-
headers[header.key] = header.value;
|
|
40
|
-
}
|
|
41
|
-
const topmostAuthResults = parsed.headers.find((header) => header.key === "authentication-results")?.value;
|
|
42
|
-
const to = (parsed.to ?? []).map((entry) => {
|
|
43
|
-
const formatted = formatAddress(entry);
|
|
44
|
-
assertSafeHeaderValue("inbound to", formatted);
|
|
45
|
-
return formatted;
|
|
46
|
-
});
|
|
47
|
-
const from = parsed.from ? formatAddress(parsed.from) : "";
|
|
48
|
-
assertSafeHeaderValue("inbound from", from);
|
|
49
|
-
const attachments = parsed.attachments.map((attachment) => {
|
|
50
|
-
return {
|
|
51
|
-
content: attachment.content,
|
|
52
|
-
disposition: attachment.disposition,
|
|
53
|
-
...attachment.encoding === void 0 ? {} : { encoding: attachment.encoding },
|
|
54
|
-
filename: attachment.filename,
|
|
55
|
-
mimeType: attachment.mimeType
|
|
56
|
-
};
|
|
57
|
-
});
|
|
58
|
-
return {
|
|
59
|
-
attachments,
|
|
60
|
-
authentication: parseAuthentication(topmostAuthResults),
|
|
61
|
-
from,
|
|
62
|
-
headers,
|
|
63
|
-
...parsed.html === void 0 ? {} : { html: parsed.html },
|
|
64
|
-
...safe("inReplyTo", parsed.inReplyTo) === void 0 ? {} : { inReplyTo: parsed.inReplyTo },
|
|
65
|
-
...safe("messageId", parsed.messageId) === void 0 ? {} : { messageId: parsed.messageId },
|
|
66
|
-
...safe("references", parsed.references) === void 0 ? {} : { references: parsed.references },
|
|
67
|
-
...safe("subject", parsed.subject) === void 0 ? {} : { subject: parsed.subject },
|
|
68
|
-
...parsed.text === void 0 ? {} : { text: parsed.text },
|
|
69
|
-
to
|
|
70
|
-
};
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
export { parseInboundEmail };
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
import { t as toAddressList, c as toAddress } from './address-vSUAVU2T.mjs';
|
|
3
|
-
|
|
4
|
-
const reasonOf = (rawError) => {
|
|
5
|
-
if (rawError instanceof Error) {
|
|
6
|
-
return rawError.message;
|
|
7
|
-
}
|
|
8
|
-
if (rawError === null || rawError === void 0) {
|
|
9
|
-
return "send failed";
|
|
10
|
-
}
|
|
11
|
-
if (typeof rawError === "string") {
|
|
12
|
-
return rawError;
|
|
13
|
-
}
|
|
14
|
-
if (typeof rawError === "number" || typeof rawError === "boolean" || typeof rawError === "bigint") {
|
|
15
|
-
return rawError.toString();
|
|
16
|
-
}
|
|
17
|
-
return JSON.stringify(rawError) ?? "send failed";
|
|
18
|
-
};
|
|
19
|
-
const requireRecipients = (to) => {
|
|
20
|
-
const list = toAddressList(to);
|
|
21
|
-
const [first] = list ?? [];
|
|
22
|
-
if (!list || first === void 0) {
|
|
23
|
-
throw new LunoraError("INTERNAL", "@lunora/mail: at least one recipient is required");
|
|
24
|
-
}
|
|
25
|
-
return { first, list };
|
|
26
|
-
};
|
|
27
|
-
const toProviderEmail = (payload, defaultFrom, to) => {
|
|
28
|
-
return {
|
|
29
|
-
bcc: toAddressList(payload.bcc),
|
|
30
|
-
cc: toAddressList(payload.cc),
|
|
31
|
-
from: toAddress(payload.from ?? defaultFrom),
|
|
32
|
-
headers: payload.headers,
|
|
33
|
-
html: payload.html,
|
|
34
|
-
replyTo: payload.replyTo ? toAddress(payload.replyTo) : void 0,
|
|
35
|
-
subject: payload.subject,
|
|
36
|
-
text: payload.text,
|
|
37
|
-
to
|
|
38
|
-
};
|
|
39
|
-
};
|
|
40
|
-
const interpretSendResult = (result) => {
|
|
41
|
-
if (!result.success || !result.data) {
|
|
42
|
-
console.error(`@lunora/mail: send failed: ${reasonOf(result.error)}`);
|
|
43
|
-
throw new LunoraError("INTERNAL", "@lunora/mail: send failed");
|
|
44
|
-
}
|
|
45
|
-
return { id: result.data.messageId };
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export { interpretSendResult as i, requireRecipients as r, toProviderEmail as t };
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { render } from '@react-email/render';
|
|
2
|
-
|
|
3
|
-
const renderEmail = async (element) => {
|
|
4
|
-
const [html, text] = await Promise.all([render(element, { pretty: false }), render(element, { plainText: true })]);
|
|
5
|
-
return { html, text };
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export { renderEmail as default };
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
|
|
3
|
-
const DEFAULT_ROOT_SHARD = "__root__";
|
|
4
|
-
const applyJurisdiction = (namespace, jurisdiction) => {
|
|
5
|
-
if (jurisdiction === void 0) {
|
|
6
|
-
return namespace;
|
|
7
|
-
}
|
|
8
|
-
if (typeof namespace.jurisdiction !== "function") {
|
|
9
|
-
throw new TypeError(
|
|
10
|
-
`@lunora/mail: Durable Object namespace does not support jurisdiction("${jurisdiction}") — update @cloudflare/workers-types or remove the jurisdiction option`
|
|
11
|
-
);
|
|
12
|
-
}
|
|
13
|
-
return namespace.jurisdiction(jurisdiction);
|
|
14
|
-
};
|
|
15
|
-
const postShardRpc = async (namespace, options) => {
|
|
16
|
-
const scoped = applyJurisdiction(namespace, options.jurisdiction);
|
|
17
|
-
const stub = scoped.get(scoped.idFromName(options.shardKey));
|
|
18
|
-
const response = await stub.fetch("https://shard.internal/rpc", {
|
|
19
|
-
body: JSON.stringify(options.envelope),
|
|
20
|
-
headers: { authorization: `Bearer ${options.adminToken}`, "content-type": "application/json" },
|
|
21
|
-
method: "POST"
|
|
22
|
-
});
|
|
23
|
-
if (response.ok === false) {
|
|
24
|
-
throw new LunoraError("INTERNAL", `${options.label} failed (HTTP ${String(response.status ?? "?")}).`);
|
|
25
|
-
}
|
|
26
|
-
const body = await response.json();
|
|
27
|
-
if (typeof body === "object" && body !== null && "error" in body) {
|
|
28
|
-
const { error } = body;
|
|
29
|
-
if (error !== void 0 && error !== null) {
|
|
30
|
-
throw new LunoraError("INTERNAL", `${options.label} returned an error: ${JSON.stringify(error)}`);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return body;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export { DEFAULT_ROOT_SHARD as D, postShardRpc as p };
|