@stacksjs/email 0.74.47 → 0.74.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/drivers/ses.js +1 -1
- package/dist/idempotency.js +1 -1
- package/dist/suppression.js +1 -1
- package/dist/webhook-dedup.js +1 -1
- package/package.json +13 -13
package/dist/drivers/ses.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{config}from"@stacksjs/config";import{
|
|
1
|
+
import{config}from"@stacksjs/config";import{templateByName}from"../template";import{buildMimeMessage}from"../mime";import{filterStringHeaders}from"../validation";import{BaseEmailDriver}from"./base";export class SESDriver extends BaseEmailDriver{name="ses";client=null;clientPromise=null;async getClient(){if(this.client)return this.client;if(!this.clientPromise)this.clientPromise=import("@stacksjs/ts-cloud").then(({SESClient})=>{const sesConfig=config?.services?.ses,explicit=sesConfig?.credentials,hasExplicit=!!(explicit?.accessKeyId&&explicit?.secretAccessKey);this.client=new SESClient(sesConfig?.region||"us-east-1",hasExplicit?{accessKeyId:explicit.accessKeyId,secretAccessKey:explicit.secretAccessKey,sessionToken:explicit.sessionToken}:void 0);return this.client});return this.clientPromise}async send(message,options){try{this.validateMessage(message);let htmlContent;if(message.template){const templ=await templateByName(message.template,options);if(templ&&"html"in templ)htmlContent=templ.html}const finalHtml=htmlContent||message.html;if(!finalHtml&&!message.text)throw Error("Email must have either HTML or text content");const fromAddress=this.formatSourceAddress({address:message.from?.address||config.email.from?.address||"",name:message.from?.name||config.email.from?.name}),toAddresses=this.formatAddresses(message.to),ccAddresses=this.formatAddresses(message.cc),bccAddresses=this.formatAddresses(message.bcc),replyToAddresses=message.replyTo?this.formatAddressList(message.replyTo):[],customHeaders=filterStringHeaders(message.headers),hasAttachments=!!(message.attachments&&message.attachments.length>0),hasCustomHeaders=!!customHeaders,client=await this.getClient();if(hasAttachments||hasCustomHeaders){const raw=buildMimeMessage({from:fromAddress,to:toAddresses.join(", "),cc:ccAddresses.length>0?ccAddresses.join(", "):void 0,replyTo:replyToAddresses.length>0?replyToAddresses.join(", "):void 0,customHeaders,subject:message.subject,text:message.text,html:finalHtml,attachments:message.attachments,messageIdDomain:config.email.domain}),result=await client.sendRawEmail({source:fromAddress,destinations:[...toAddresses,...ccAddresses,...bccAddresses],rawMessage:raw});return this.handleSuccess(message,result.MessageId)}const body={};if(finalHtml)body.Html={Charset:config.email.charset||"UTF-8",Data:finalHtml};if(message.text)body.Text={Charset:config.email.charset||"UTF-8",Data:message.text};const result=await client.sendEmail({FromEmailAddress:fromAddress,Destination:{ToAddresses:toAddresses,CcAddresses:ccAddresses,BccAddresses:bccAddresses},...replyToAddresses.length>0?{ReplyToAddresses:replyToAddresses}:{},Content:{Simple:{Subject:{Charset:config.email.charset||"UTF-8",Data:message.subject},Body:body}}});return this.handleSuccess(message,result.MessageId)}catch(error){return this.handleError(this.enrichSesError(error),message)}}formatSourceAddress(from){if(!from.name)return from.address;return`${/[",()<>[\]:;@\\]/.test(from.name)?`"${from.name.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`:from.name} <${from.address}>`}enrichSesError(error){const err=error instanceof Error?error:Error(String(error)),text=`${err.message} ${err.name??""}`.toLowerCase(),region=config?.services?.ses?.region||"us-east-1";if(text.includes("email address is not verified")||text.includes("not authorized to send")||text.includes("messagerejected")&&text.includes("verified")){err.message=`${err.message}
|
|
2
2
|
|
|
3
3
|
SES sandbox restriction: the From and (in sandbox) every To address must be verified. Verify identities in the SES console under "Verified identities" (region: ${region}), or request production access to lift the recipient restriction.`;return err}if(text.includes("signaturedoesnotmatch")||text.includes("invalidclienttokenid")||text.includes("unable to locate credentials")||text.includes("the security token included in the request is invalid")){err.message=`${err.message}
|
|
4
4
|
|
package/dist/idempotency.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{db}from"@stacksjs/database";let warnedAboutMissingEmailIdempotencyTable=!1;function warnOnceAboutMissingTable(){if(warnedAboutMissingEmailIdempotencyTable)return;warnedAboutMissingEmailIdempotencyTable=!0;console.warn("[email/idempotency] email_idempotency table missing - idempotency keys are accepted but NOT enforced. Run migrations to enable dedup.")}function isMissingTableError(err){const msg=(err?.message??"").toLowerCase();return msg.includes("no such table")||msg.includes("doesn't exist")||msg.includes("connection closed")||msg.includes("unable to open database")||msg.includes("econnrefused")||msg.includes("connection terminated")||msg.includes("no database")||msg.includes("database connection")}export async function findEmailByIdempotencyKey(key){try{const row=await db.selectFrom("email_idempotency").where("idempotency_key","=",key).selectAll().executeTakeFirst();if(!row)return null;return{success:Boolean(row.success),message:`Idempotent replay - original send recorded ${row.created_at}`,provider:String(row.provider??"cache"),messageId:row.message_id==null?void 0:String(row.message_id)}}catch(err){if(isMissingTableError(err)){warnOnceAboutMissingTable();return null}throw err}}export async function recordEmailIdempotency(key,message,result){if(!result.success)return;const recipient=Array.isArray(message.to)?message.to.map((t)=>typeof t==="string"?t:t.address).join(", "):typeof message.to==="string"?message.to:message.to.address;try{await db.insertInto("email_idempotency").values({idempotency_key:key,message_id:result.messageId??null,recipient,subject:message.subject,provider:result.provider,success:1,created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute()}catch(err){if(isMissingTableError(err)){warnOnceAboutMissingTable();return}const msg=err?.message??"";if(msg.includes("UNIQUE constraint")||msg.includes("Duplicate entry"))return;throw err}}
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";let warnedAboutMissingEmailIdempotencyTable=!1;function warnOnceAboutMissingTable(){if(warnedAboutMissingEmailIdempotencyTable)return;warnedAboutMissingEmailIdempotencyTable=!0;console.warn("[email/idempotency] email_idempotency table missing - idempotency keys are accepted but NOT enforced. Run migrations to enable dedup.")}function isMissingTableError(err){const msg=(err?.message??"").toLowerCase();return msg.includes("no such table")||msg.includes("doesn't exist")||msg.includes("connection closed")||msg.includes("unable to open database")||msg.includes("econnrefused")||msg.includes("connection terminated")||msg.includes("no database")||msg.includes("database connection")}export async function findEmailByIdempotencyKey(key){try{const row=await db.selectFrom("email_idempotency").where("idempotency_key","=",key).selectAll().executeTakeFirst();if(!row)return null;return{success:Boolean(row.success),message:`Idempotent replay - original send recorded ${row.created_at}`,provider:String(row.provider??"cache"),messageId:row.message_id==null?void 0:String(row.message_id)}}catch(err){if(isMissingTableError(err)){warnOnceAboutMissingTable();return null}throw err}}export async function recordEmailIdempotency(key,message,result){if(!result.success)return;const recipient=Array.isArray(message.to)?message.to.map((t)=>typeof t==="string"?t:t.address).join(", "):typeof message.to==="string"?message.to:message.to.address;try{await db.insertInto("email_idempotency").values({idempotency_key:key,message_id:result.messageId??null,recipient,subject:message.subject,provider:result.provider,success:1,created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute()}catch(err){if(isMissingTableError(err)){warnOnceAboutMissingTable();return}const msg=err?.message??"";if(msg.includes("UNIQUE constraint")||msg.includes("Duplicate entry"))return;throw err}}
|
package/dist/suppression.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{db}from"@stacksjs/database";let warnedAboutMissingTable=!1;function warnOnceAboutMissingTable(){if(warnedAboutMissingTable)return;warnedAboutMissingTable=!0;console.warn("[email/suppression] email_suppressions table missing - suppression checks accepted but NOT enforced. Run migrations to enable enforcement.")}export function isMissingTableError(err){const e=err,msg=e?.message??"";return e?.code==="42P01"||msg.includes("no such table")||msg.includes("doesn't exist")||/relation "[^"]*" does not exist/i.test(msg)}function isSuppressionStoreUnavailable(err){if(isMissingTableError(err))return!0;const msg=(err?.message??"").toLowerCase();return msg.includes("connection closed")||msg.includes("unable to open database")||msg.includes("econnrefused")||msg.includes("connection terminated")||msg.includes("no database")||msg.includes("database connection")}function canonicalize(email){return String(email).trim().toLowerCase()}export async function isSuppressed(email,type){const canon=canonicalize(email);try{let query=db.selectFrom("email_suppressions").where("email","=",canon).select(["email"]);if(type)query=query.where("type","=",type);const row=await query.executeTakeFirst();return Boolean(row)}catch(err){if(isSuppressionStoreUnavailable(err)){warnOnceAboutMissingTable();return!1}throw err}}export async function getSuppressions(email){const canon=canonicalize(email);try{return await db.selectFrom("email_suppressions").where("email","=",canon).selectAll().execute()??[]}catch(err){if(isSuppressionStoreUnavailable(err)){warnOnceAboutMissingTable();return[]}throw err}}export async function suppress(email,type,reason){const canon=canonicalize(email),createdAt=new Date().toISOString().slice(0,19).replace("T"," ");try{await db.insertInto("email_suppressions").values({email:canon,type,reason:reason??null,created_at:createdAt}).execute()}catch(err){if(isSuppressionStoreUnavailable(err)){warnOnceAboutMissingTable();return}const msg=err?.message??"";if(msg.includes("UNIQUE constraint")||msg.includes("Duplicate entry"))return;throw err}}export async function unsuppress(email,type){const canon=canonicalize(email);try{await db.deleteFrom("email_suppressions").where("email","=",canon).where("type","=",type).execute()}catch(err){if(isSuppressionStoreUnavailable(err)){warnOnceAboutMissingTable();return}throw err}}export async function getSuppressionPolicy(){try{const{config}=await import("@stacksjs/config"),policy=config?.email?.suppressionPolicy;if(policy==="strict"||policy==="transactional-allowed"||policy==="off")return policy}catch{}return"strict"}export async function checkSuppressionFor(email,tag){const policy=await getSuppressionPolicy();if(policy==="off")return null;if(policy==="transactional-allowed"&&tag==="transactional")return null;const suppressions=await getSuppressions(email);if(suppressions.length===0)return null;const priority=["unsubscribe","complaint","bounce","manual"];for(const p of priority)if(suppressions.some((s)=>s.type===p))return p;return suppressions[0].type}
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";let warnedAboutMissingTable=!1;function warnOnceAboutMissingTable(){if(warnedAboutMissingTable)return;warnedAboutMissingTable=!0;console.warn("[email/suppression] email_suppressions table missing - suppression checks accepted but NOT enforced. Run migrations to enable enforcement.")}export function isMissingTableError(err){const e=err,msg=e?.message??"";return e?.code==="42P01"||msg.includes("no such table")||msg.includes("doesn't exist")||/relation "[^"]*" does not exist/i.test(msg)}function isSuppressionStoreUnavailable(err){if(isMissingTableError(err))return!0;const msg=(err?.message??"").toLowerCase();return msg.includes("connection closed")||msg.includes("unable to open database")||msg.includes("econnrefused")||msg.includes("connection terminated")||msg.includes("no database")||msg.includes("database connection")}function canonicalize(email){return String(email).trim().toLowerCase()}export async function isSuppressed(email,type){const canon=canonicalize(email);try{let query=db.selectFrom("email_suppressions").where("email","=",canon).select(["email"]);if(type)query=query.where("type","=",type);const row=await query.executeTakeFirst();return Boolean(row)}catch(err){if(isSuppressionStoreUnavailable(err)){warnOnceAboutMissingTable();return!1}throw err}}export async function getSuppressions(email){const canon=canonicalize(email);try{return await db.selectFrom("email_suppressions").where("email","=",canon).selectAll().execute()??[]}catch(err){if(isSuppressionStoreUnavailable(err)){warnOnceAboutMissingTable();return[]}throw err}}export async function suppress(email,type,reason){const canon=canonicalize(email),createdAt=new Date().toISOString().slice(0,19).replace("T"," ");try{await db.insertInto("email_suppressions").values({email:canon,type,reason:reason??null,created_at:createdAt}).execute()}catch(err){if(isSuppressionStoreUnavailable(err)){warnOnceAboutMissingTable();return}const msg=err?.message??"";if(msg.includes("UNIQUE constraint")||msg.includes("Duplicate entry"))return;throw err}}export async function unsuppress(email,type){const canon=canonicalize(email);try{await db.deleteFrom("email_suppressions").where("email","=",canon).where("type","=",type).execute()}catch(err){if(isSuppressionStoreUnavailable(err)){warnOnceAboutMissingTable();return}throw err}}export async function getSuppressionPolicy(){try{const{config}=await import("@stacksjs/config"),policy=config?.email?.suppressionPolicy;if(policy==="strict"||policy==="transactional-allowed"||policy==="off")return policy}catch{}return"strict"}export async function checkSuppressionFor(email,tag){const policy=await getSuppressionPolicy();if(policy==="off")return null;if(policy==="transactional-allowed"&&tag==="transactional")return null;const suppressions=await getSuppressions(email);if(suppressions.length===0)return null;const priority=["unsubscribe","complaint","bounce","manual"];for(const p of priority)if(suppressions.some((s)=>s.type===p))return p;return suppressions[0].type}
|
package/dist/webhook-dedup.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{db}from"@stacksjs/database";let warnedAboutMissingTable=!1;function warnOnceAboutMissingTable(){if(warnedAboutMissingTable)return;warnedAboutMissingTable=!0;console.warn("[email/webhook-dedup] email_webhook_events table missing - webhook idempotency NOT enforced. Providers may double-deliver retries; run migrations to enable dedup.")}function isMissingTableError(err){const msg=err?.message??"";return msg.includes("no such table")||msg.includes("doesn't exist")}export async function recordWebhookEventOrSkip(provider,eventId){if(!eventId)return!0;try{await db.insertInto("email_webhook_events").values({provider,event_id:eventId,processed_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute();return!0}catch(err){if(isMissingTableError(err)){warnOnceAboutMissingTable();return!0}const msg=err?.message??"";if(msg.includes("UNIQUE constraint")||msg.includes("Duplicate entry"))return!1;throw err}}
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";let warnedAboutMissingTable=!1;function warnOnceAboutMissingTable(){if(warnedAboutMissingTable)return;warnedAboutMissingTable=!0;console.warn("[email/webhook-dedup] email_webhook_events table missing - webhook idempotency NOT enforced. Providers may double-deliver retries; run migrations to enable dedup.")}function isMissingTableError(err){const msg=err?.message??"";return msg.includes("no such table")||msg.includes("doesn't exist")}export async function recordWebhookEventOrSkip(provider,eventId){if(!eventId)return!0;try{await db.insertInto("email_webhook_events").values({provider,event_id:eventId,processed_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute();return!0}catch(err){if(isMissingTableError(err)){warnOnceAboutMissingTable();return!0}const msg=err?.message??"";if(msg.includes("UNIQUE constraint")||msg.includes("Duplicate entry"))return!1;throw err}}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/email",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.74.
|
|
5
|
+
"version": "0.74.50",
|
|
6
6
|
"description": "The Stacks Email integration. Painlessly create & manage your inboxes, templates, and send emails.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -61,26 +61,26 @@
|
|
|
61
61
|
"prepublishOnly": "bun run build"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@stacksjs/config": "0.74.
|
|
65
|
-
"@stacksjs/database": "0.74.
|
|
66
|
-
"@stacksjs/events": "0.74.
|
|
67
|
-
"@stacksjs/logging": "0.74.
|
|
64
|
+
"@stacksjs/config": "0.74.50",
|
|
65
|
+
"@stacksjs/database": "0.74.50",
|
|
66
|
+
"@stacksjs/events": "0.74.50",
|
|
67
|
+
"@stacksjs/logging": "0.74.50",
|
|
68
68
|
"@stacksjs/mail": "^0.3.6",
|
|
69
|
-
"@stacksjs/path": "0.74.
|
|
70
|
-
"@stacksjs/storage": "0.74.
|
|
71
|
-
"@stacksjs/strings": "0.74.
|
|
69
|
+
"@stacksjs/path": "0.74.50",
|
|
70
|
+
"@stacksjs/storage": "0.74.50",
|
|
71
|
+
"@stacksjs/strings": "0.74.50",
|
|
72
72
|
"@stacksjs/stx": "^0.2.286",
|
|
73
73
|
"@stacksjs/ts-cloud": "^0.16.0",
|
|
74
|
-
"@stacksjs/utils": "0.74.
|
|
74
|
+
"@stacksjs/utils": "0.74.50"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@stacksjs/cli": "0.74.
|
|
78
|
-
"@stacksjs/error-handling": "0.74.
|
|
79
|
-
"@stacksjs/types": "0.74.
|
|
77
|
+
"@stacksjs/cli": "0.74.50",
|
|
78
|
+
"@stacksjs/error-handling": "0.74.50",
|
|
79
|
+
"@stacksjs/types": "0.74.50",
|
|
80
80
|
"better-dx": "^0.2.24"
|
|
81
81
|
},
|
|
82
82
|
"peerDependencies": {
|
|
83
|
-
"@stacksjs/queue": "0.74.
|
|
83
|
+
"@stacksjs/queue": "0.74.50"
|
|
84
84
|
},
|
|
85
85
|
"peerDependenciesMeta": {
|
|
86
86
|
"@stacksjs/queue": {
|