@lunora/mail 1.0.0-alpha.59 → 1.0.0-alpha.60

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.
@@ -26,6 +26,16 @@ interface InboundAttachment {
26
26
  * Verdicts are best-effort: when the receiving MX did not stamp an
27
27
  * `Authentication-Results` header, every field is `null` ("unknown").
28
28
  *
29
+ * SECURITY: a bare `"pass"` is NOT proof the `From` header is genuine. SPF
30
+ * authenticates the envelope `MAIL FROM` domain and DKIM the signing domain
31
+ * (`d=`), and an attacker controls both — `spf=pass smtp.mailfrom=evil.example;
32
+ * dkim=pass header.d=evil.example` is routine for a message whose `From` says
33
+ * `ceo@victim.example`. Each verdict therefore carries the identifier it is
34
+ * about (`*Domain`): an SPF or DKIM pass only vouches for `from` when that
35
+ * domain equals the `From` address's domain (RFC 7489 strict alignment). Only a
36
+ * DMARC pass already checked alignment for you. A pass with no identifier
37
+ * reported cannot be aligned and must be treated as unauthenticated.
38
+ *
29
39
  * SECURITY: verdicts are read from the **first/topmost** `Authentication-Results`
30
40
  * header in document order. The receiving MX prepends its own genuine header per
31
41
  * RFC 8601, so the topmost occurrence is the trustworthy one; any lower
@@ -37,20 +47,28 @@ interface InboundAttachment {
37
47
  interface InboundAuthentication {
38
48
  /** DKIM verdict (`"pass"`/`"fail"`/…), or `null` when not reported. */
39
49
  dkim: string | null;
50
+ /** Signing domain the DKIM verdict is about (`header.d=`, lowercased), or `null` when not reported. */
51
+ dkimDomain: string | null;
40
52
  /** DMARC verdict (`"pass"`/`"fail"`/…), or `null` when not reported. */
41
53
  dmarc: string | null;
54
+ /** `From` domain the DMARC verdict evaluated (`header.from=`, lowercased), or `null` when not reported. */
55
+ dmarcDomain: string | null;
42
56
  /** SPF verdict (`"pass"`/`"fail"`/…), or `null` when not reported. */
43
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;
44
60
  }
45
61
  /** Normalised, transport-agnostic view of a received message. */
46
62
  interface InboundEmail {
47
63
  /** Decoded attachments (empty array when none). */
48
64
  attachments: InboundAttachment[];
49
65
  /**
50
- * Sender-authentication verdicts (DKIM/SPF/DMARC) parsed from the receiving
51
- * MX's **first/topmost** `Authentication-Results` header. SECURITY: see
52
- * {@link InboundAuthentication} — `from` is spoofable; gate trust on these
53
- * verdicts, not on `from`. Reading the raw `headers["authentication-results"]`
66
+ * Sender-authentication verdicts (DKIM/SPF/DMARC) and the domain each is
67
+ * about, parsed from the receiving MX's **first/topmost**
68
+ * `Authentication-Results` header. SECURITY: see
69
+ * {@link InboundAuthentication} `from` is spoofable, and an SPF/DKIM pass
70
+ * vouches for it only when its `*Domain` equals the `From` domain (a DMARC
71
+ * pass checked that already). Reading the raw `headers["authentication-results"]`
54
72
  * map instead exposes last-wins (a lower, potentially attacker-injected)
55
73
  * value — trust `authentication`, not the raw map.
56
74
  */
@@ -26,6 +26,16 @@ interface InboundAttachment {
26
26
  * Verdicts are best-effort: when the receiving MX did not stamp an
27
27
  * `Authentication-Results` header, every field is `null` ("unknown").
28
28
  *
29
+ * SECURITY: a bare `"pass"` is NOT proof the `From` header is genuine. SPF
30
+ * authenticates the envelope `MAIL FROM` domain and DKIM the signing domain
31
+ * (`d=`), and an attacker controls both — `spf=pass smtp.mailfrom=evil.example;
32
+ * dkim=pass header.d=evil.example` is routine for a message whose `From` says
33
+ * `ceo@victim.example`. Each verdict therefore carries the identifier it is
34
+ * about (`*Domain`): an SPF or DKIM pass only vouches for `from` when that
35
+ * domain equals the `From` address's domain (RFC 7489 strict alignment). Only a
36
+ * DMARC pass already checked alignment for you. A pass with no identifier
37
+ * reported cannot be aligned and must be treated as unauthenticated.
38
+ *
29
39
  * SECURITY: verdicts are read from the **first/topmost** `Authentication-Results`
30
40
  * header in document order. The receiving MX prepends its own genuine header per
31
41
  * RFC 8601, so the topmost occurrence is the trustworthy one; any lower
@@ -37,20 +47,28 @@ interface InboundAttachment {
37
47
  interface InboundAuthentication {
38
48
  /** DKIM verdict (`"pass"`/`"fail"`/…), or `null` when not reported. */
39
49
  dkim: string | null;
50
+ /** Signing domain the DKIM verdict is about (`header.d=`, lowercased), or `null` when not reported. */
51
+ dkimDomain: string | null;
40
52
  /** DMARC verdict (`"pass"`/`"fail"`/…), or `null` when not reported. */
41
53
  dmarc: string | null;
54
+ /** `From` domain the DMARC verdict evaluated (`header.from=`, lowercased), or `null` when not reported. */
55
+ dmarcDomain: string | null;
42
56
  /** SPF verdict (`"pass"`/`"fail"`/…), or `null` when not reported. */
43
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;
44
60
  }
45
61
  /** Normalised, transport-agnostic view of a received message. */
46
62
  interface InboundEmail {
47
63
  /** Decoded attachments (empty array when none). */
48
64
  attachments: InboundAttachment[];
49
65
  /**
50
- * Sender-authentication verdicts (DKIM/SPF/DMARC) parsed from the receiving
51
- * MX's **first/topmost** `Authentication-Results` header. SECURITY: see
52
- * {@link InboundAuthentication} — `from` is spoofable; gate trust on these
53
- * verdicts, not on `from`. Reading the raw `headers["authentication-results"]`
66
+ * Sender-authentication verdicts (DKIM/SPF/DMARC) and the domain each is
67
+ * about, parsed from the receiving MX's **first/topmost**
68
+ * `Authentication-Results` header. SECURITY: see
69
+ * {@link InboundAuthentication} `from` is spoofable, and an SPF/DKIM pass
70
+ * vouches for it only when its `*Domain` equals the `From` domain (a DMARC
71
+ * pass checked that already). Reading the raw `headers["authentication-results"]`
54
72
  * map instead exposes last-wins (a lower, potentially attacker-injected)
55
73
  * value — trust `authentication`, not the raw map.
56
74
  */
@@ -1 +1 @@
1
- import{createInboundEmailHandler as r,dispatchToLunoraFunction as a}from"../packem_shared/createInboundEmailHandler-C6V47mz5.mjs";import{parseInboundEmail as t}from"../packem_shared/parseInboundEmail-CqU52y3O.mjs";export{r as createInboundEmailHandler,a as dispatchToLunoraFunction,t as parseInboundEmail};
1
+ import{createInboundEmailHandler as r,dispatchToLunoraFunction as a}from"../packem_shared/createInboundEmailHandler-C6V47mz5.mjs";import{parseInboundEmail as t}from"../packem_shared/parseInboundEmail-BG0NU_e9.mjs";export{r as createInboundEmailHandler,a as dispatchToLunoraFunction,t as parseInboundEmail};
@@ -0,0 +1 @@
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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/mail",
3
- "version": "1.0.0-alpha.59",
3
+ "version": "1.0.0-alpha.60",
4
4
  "description": "Email for Lunora: Resend adapter, TSX templates, and queue-backed sends",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -1 +0,0 @@
1
- import u from"postal-mime";import{a as r}from"./address-soPj2Z0j.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};