@stacksjs/email 0.70.353 → 0.70.355
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-parser.d.ts +25 -0
- package/dist/inbound-parser.js +1 -0
- package/dist/inbox-mailbox.d.ts +16 -0
- package/dist/inbox-mailbox.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/mime-preview.d.ts +2 -0
- package/dist/mime-preview.js +5 -1
- package/dist/sdk/inbox-attachments.d.ts +20 -0
- package/dist/sdk/inbox-attachments.js +1 -0
- package/dist/sdk/index.d.ts +18 -1
- package/dist/sdk/index.js +1 -1
- package/package.json +7 -6
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { inboundMailboxRecipient } from './inbox-mailbox';
|
|
2
|
+
declare function mailbox(address: Address | undefined): Mailbox | undefined;
|
|
3
|
+
export declare function inboundAttachmentName(filename: string | null | undefined, index: number): string;
|
|
4
|
+
export declare function inboundAttachmentStorageName(filename: string, index: number): string;
|
|
5
|
+
export declare function inboundMessageStorageId(messageId: string): string;
|
|
6
|
+
export declare function parseInboundEmail(rawEmail: string | Uint8Array): Promise<ParsedInboundEmail>;
|
|
7
|
+
export declare interface ParsedInboundAttachment {
|
|
8
|
+
name: string
|
|
9
|
+
storageName: string
|
|
10
|
+
contentType: string
|
|
11
|
+
content: Uint8Array
|
|
12
|
+
contentId?: string
|
|
13
|
+
disposition?: string
|
|
14
|
+
}
|
|
15
|
+
export declare interface ParsedInboundEmail {
|
|
16
|
+
from: string
|
|
17
|
+
fromName: string
|
|
18
|
+
recipients: string[]
|
|
19
|
+
subject: string
|
|
20
|
+
date?: string
|
|
21
|
+
html: string
|
|
22
|
+
text: string
|
|
23
|
+
attachments: ParsedInboundAttachment[]
|
|
24
|
+
}
|
|
25
|
+
export { inboundMailboxRecipient } from './inbox-mailbox';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import PostalMime from"postal-mime";import{inboundMailboxRecipient}from"./inbox-mailbox";import{inboxAttachmentContentType}from"./sdk/inbox-attachments";export{inboundMailboxRecipient}from"./inbox-mailbox";const MAX_RAW_EMAIL_BYTES=52428800,MAX_ATTACHMENTS=1000;function flattenAddresses(addresses){return(addresses||[]).flatMap((address)=>{if("group"in address&&Array.isArray(address.group))return address.group;return[address]})}function mailbox(address){if(!address)return;if("group"in address&&Array.isArray(address.group))return address.group[0];return address}export function inboundAttachmentName(filename,index){return String(filename||"").replace(/[\u0000-\u001F\u007F]/g,"").replace(/[\\/]/g,"_").trim().slice(0,220)||`attachment-${index+1}`}export function inboundAttachmentStorageName(filename,index){return`stacks-${String(index+1).padStart(4,"0")}--${encodeURIComponent(filename)}`}export function inboundMessageStorageId(messageId){const normalized=messageId.replace(/[\u0000-\u001F\u007F\\/]/g,"_").trim().slice(0,240);if(!normalized||normalized==="."||normalized==="..")throw TypeError("Inbound email object has no safe message identifier.");return normalized}export async function parseInboundEmail(rawEmail){if((typeof rawEmail==="string"?Buffer.byteLength(rawEmail):rawEmail.byteLength)>MAX_RAW_EMAIL_BYTES)throw RangeError(`Raw email exceeds the ${MAX_RAW_EMAIL_BYTES} byte parsing limit.`);const parsed=await PostalMime.parse(rawEmail,{attachmentEncoding:"arraybuffer",maxHeadersSize:524288,maxNestingDepth:64,maxRfc822NestingDepth:4});if(parsed.attachments.length>MAX_ATTACHMENTS)throw RangeError(`Email contains more than ${MAX_ATTACHMENTS} attachments.`);const sender=mailbox(parsed.from),recipients=flattenAddresses(parsed.to).map((recipient)=>recipient.address?.trim().toLowerCase()).filter((address)=>Boolean(address));if(parsed.deliveredTo)recipients.push(parsed.deliveredTo.trim().toLowerCase());const attachments=parsed.attachments.map((attachment,index)=>{const name=inboundAttachmentName(attachment.filename,index),content=typeof attachment.content==="string"?new TextEncoder().encode(attachment.content):new Uint8Array(attachment.content);return{name,storageName:inboundAttachmentStorageName(name,index),contentType:inboxAttachmentContentType(attachment.mimeType),content,...attachment.contentId?{contentId:attachment.contentId}:{},...attachment.disposition?{disposition:attachment.disposition}:{}}});return{from:sender?.address?.trim().toLowerCase()||"",fromName:sender?.name?.trim()||"",recipients:[...new Set(recipients)],subject:parsed.subject?.trim()||"No Subject",...parsed.date?{date:parsed.date}:{},html:parsed.html?.trim()||"",text:parsed.text?.trim()||"",attachments}}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare function inboundMailboxRecipient(address: string, expectedDomain: string): { address: string, domain: string, localPart: string } | null;
|
|
2
|
+
export declare function inboxMailboxPath(mailbox: string, expectedDomain: string): InboxMailboxPath;
|
|
3
|
+
export declare function inboxMessagePath(path: string, mailbox: InboxMailboxPath): string;
|
|
4
|
+
export declare interface InboxMailboxPath {
|
|
5
|
+
address: string
|
|
6
|
+
domain: string
|
|
7
|
+
localPart: string
|
|
8
|
+
prefix: string
|
|
9
|
+
indexKey: string
|
|
10
|
+
}
|
|
11
|
+
export declare class InvalidInboxMailboxError extends TypeError {
|
|
12
|
+
constructor(message?: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class InvalidInboxPathError extends TypeError {
|
|
15
|
+
constructor(message?: string);
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class InvalidInboxMailboxError extends TypeError{constructor(message="Mailbox must be a valid address on the configured email domain."){super(message);this.name="InvalidInboxMailboxError"}}export class InvalidInboxPathError extends TypeError{constructor(message="Inbox metadata contains an invalid message path."){super(message);this.name="InvalidInboxPathError"}}export function inboundMailboxRecipient(address,expectedDomain){const normalizedAddress=address.trim().toLowerCase(),separator=normalizedAddress.lastIndexOf("@");if(separator<=0)return null;const localPart=normalizedAddress.slice(0,separator),domain=normalizedAddress.slice(separator+1);if(domain!==expectedDomain.trim().toLowerCase())return null;if(!/^[a-z0-9.!#$%&'*+?^_`{|}~=-]+$/i.test(localPart)||localPart.includes(".."))return null;if(!/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))*$/i.test(domain))return null;return{address:normalizedAddress,domain,localPart}}export function inboxMailboxPath(mailbox,expectedDomain){const address=mailbox.includes("@")?mailbox:`${mailbox}@${expectedDomain}`,parsed=inboundMailboxRecipient(address,expectedDomain);if(!parsed)throw new InvalidInboxMailboxError;const prefix=`mailboxes/${parsed.domain}/${parsed.localPart}`;return{...parsed,prefix,indexKey:`${prefix}/inbox.json`}}export function inboxMessagePath(path,mailbox){const normalized=path.trim(),prefix=`${mailbox.prefix}/`;if(!normalized.startsWith(prefix))throw new InvalidInboxPathError;const segments=normalized.slice(prefix.length).split("/");if(segments.length===0||segments.some((segment)=>!segment||segment==="."||segment===".."||segment.includes("\\")||/[\u0000-\u001F\u007F]/.test(segment)))throw new InvalidInboxPathError;return normalized}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export type { DiscoveredMailable, MailablePreview } from './preview';
|
|
|
3
3
|
export * from './drivers/index';
|
|
4
4
|
export * from './email';
|
|
5
5
|
export * from './idempotency';
|
|
6
|
+
export * from './inbox-mailbox';
|
|
7
|
+
export * from './inbound-parser';
|
|
6
8
|
export * from './suppression';
|
|
7
9
|
export * from './unsubscribe';
|
|
8
10
|
export * from './webhook-dedup';
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export*from"./drivers";export*from"./email";export*from"./idempotency";export*from"./suppression";export*from"./unsubscribe";export*from"./webhook-dedup";export*from"./webhook-events";export*from"./webhook-handlers";export*from"./webhook-signatures";export*from"./mailable";export*from"./mime-preview";export*from"./sdk";export*from"./template";export*from"./types";export{inlineCss,shouldInlineByDefault}from"./css-inliner";export{discoverMailables,loadSampleProps,renderMailablePreview}from"./preview";export{renderIndexHtml,renderPreviewHtml}from"./preview-ui";
|
|
1
|
+
export*from"./drivers";export*from"./email";export*from"./idempotency";export*from"./inbox-mailbox";export*from"./inbound-parser";export*from"./suppression";export*from"./unsubscribe";export*from"./webhook-dedup";export*from"./webhook-events";export*from"./webhook-handlers";export*from"./webhook-signatures";export*from"./mailable";export*from"./mime-preview";export*from"./sdk";export*from"./template";export*from"./types";export{inlineCss,shouldInlineByDefault}from"./css-inliner";export{discoverMailables,loadSampleProps,renderMailablePreview}from"./preview";export{renderIndexHtml,renderPreviewHtml}from"./preview-ui";
|
package/dist/mime-preview.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export declare function extractEmailPreview(rawEmail: string, maxLength?: number): string;
|
|
2
2
|
export declare function normalizeEmailPreview(preview: string, maxLength?: number): string;
|
|
3
|
+
export declare function normalizeEmailTextBody(body: string): string;
|
|
4
|
+
export declare function normalizeEmailHtmlBody(body: string): string;
|
package/dist/mime-preview.js
CHANGED
|
@@ -2,4 +2,8 @@ function splitHeaders(value){const normalized=value.replace(/\r\n/g,`
|
|
|
2
2
|
`),separator=normalized.indexOf(`
|
|
3
3
|
|
|
4
4
|
`);if(separator<0)return{headers:{},body:normalized};const headerLines=normalized.slice(0,separator).replace(/\n[ \t]+/g," ").split(`
|
|
5
|
-
`),headers={};for(const line of headerLines){const match=line.match(/^([^:]+):\s*(.*)$/);if(match)headers[match[1].toLowerCase()]=match[2]||""}return{headers,body:normalized.slice(separator+2)}}function decodeQuotedPrintable(value){const input=value.replace(
|
|
5
|
+
`),headers={};for(const line of headerLines){const match=line.match(/^([^:]+):\s*(.*)$/);if(match)headers[match[1].toLowerCase()]=match[2]||""}return{headers,body:normalized.slice(separator+2)}}function decodeQuotedPrintable(value){const input=value.replace(/\r\n/g,`
|
|
6
|
+
`).replace(/=\n/g,""),bytes=[],encoder=new TextEncoder;for(let index=0;index<input.length;index++){const encoded=input.slice(index,index+3);if(/^=[0-9a-f]{2}$/i.test(encoded)){bytes.push(Number.parseInt(encoded.slice(1),16));index+=2}else bytes.push(...encoder.encode(input[index]))}return new TextDecoder().decode(Uint8Array.from(bytes))}function normalizedMimeBody(value,requestedType){const normalized=value.replace(/\r\n/g,`
|
|
7
|
+
`).trim(),fragmentBoundary=normalized.match(/^--([^\n]+)\n/)?.[1]?.replace(/--$/,""),hasMimeHeaders=/^(?:content-type|content-transfer-encoding|mime-version):/i.test(normalized),source=fragmentBoundary?`Content-Type: multipart/mixed; boundary="${fragmentBoundary.replace(/["\\]/g,"")}"
|
|
8
|
+
|
|
9
|
+
${normalized}`:normalized;if(fragmentBoundary||hasMimeHeaders){const selected=mimeParts(source).find((part)=>part.contentType.startsWith(requestedType));if(selected)return decodeBody(selected.body,selected.encoding).trim()}return decodeQuotedPrintable(stripLegacyMimePreamble(normalized)).trim()}function decodeBody(body,encoding){if(encoding==="base64")try{return Buffer.from(body.replace(/\s+/g,""),"base64").toString("utf8")}catch{return body}if(encoding==="quoted-printable")return decodeQuotedPrintable(body);return body}function mimeParts(value){const{headers,body}=splitHeaders(value),contentType=(headers["content-type"]||"text/plain").toLowerCase(),boundary=headers["content-type"]?.match(/boundary\s*=\s*(?:"([^"]+)"|([^;\s]+))/i)?.slice(1).find(Boolean);if(contentType.startsWith("multipart/")&&boundary)return body.split(`--${boundary}`).slice(1).filter((part)=>!part.trimStart().startsWith("--")).flatMap(mimeParts);return[{contentType,encoding:(headers["content-transfer-encoding"]||"").toLowerCase(),body}]}function stripLegacyMimePreamble(value){let result=value.trim();result=result.replace(/^[-=_]{2,}[^\s]*\s+/,"");for(let pass=0;pass<6;pass++){const next=result.replace(/^content-transfer-encoding:\s*[^\s]+\s*/i,"").replace(/^mime-version:\s*[^\s]+\s*/i,"").replace(/^content-description:\s*[^\s]+\s*/i,"").replace(/^content-type:\s*[^\s;]+(?:;\s*(?:charset|boundary|name)\s*=\s*(?:"[^"]*"|[^\s]+))*\s*/i,"");if(next===result)break;result=next}return result.replace(/^[-=_]{4,}[^\s]*\s*/,"")}function plainText(value){return decodeQuotedPrintable(stripLegacyMimePreamble(value)).replace(/<style\b[\s\S]*?<\/style>/gi," ").replace(/<script\b[\s\S]*?<\/script>/gi," ").replace(/<[^>]+>/g," ").replace(/ /gi," ").replace(/&/gi,"&").replace(/</gi,"<").replace(/>/gi,">").replace(/"/gi,'"').replace(/'/gi,"'").replace(/\s+/g," ").trim()}export function extractEmailPreview(rawEmail,maxLength=200){const parts=mimeParts(rawEmail),selected=parts.find((part)=>part.contentType.startsWith("text/plain"))||parts.find((part)=>part.contentType.startsWith("text/html"));return plainText(selected?decodeBody(selected.body,selected.encoding):rawEmail).slice(0,maxLength)}export function normalizeEmailPreview(preview,maxLength=200){return plainText(preview).slice(0,maxLength)}export function normalizeEmailTextBody(body){return normalizedMimeBody(body,"text/plain")}export function normalizeEmailHtmlBody(body){return normalizedMimeBody(body,"text/html")}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare function inboxAttachmentPrefix(basePath: string): string;
|
|
2
|
+
export declare function inboxAttachmentId(key: string): string;
|
|
3
|
+
export declare function inboxAttachmentName(key: string, prefix: string): string;
|
|
4
|
+
export declare function mapInboxAttachmentObjects(basePath: string, objects: InboxAttachmentObject[]): StoredInboxAttachment[];
|
|
5
|
+
export declare function inboxAttachmentContentDisposition(filename: string): string;
|
|
6
|
+
export declare function inboxAttachmentContentType(contentType?: string): string;
|
|
7
|
+
export declare interface InboxAttachment {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
size: number
|
|
11
|
+
lastModified?: string
|
|
12
|
+
}
|
|
13
|
+
export declare interface StoredInboxAttachment extends InboxAttachment {
|
|
14
|
+
key: string
|
|
15
|
+
}
|
|
16
|
+
export declare interface InboxAttachmentObject {
|
|
17
|
+
Key: string
|
|
18
|
+
Size?: number
|
|
19
|
+
LastModified?: string
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createHash}from"node:crypto";export function inboxAttachmentPrefix(basePath){return`${basePath.replace(/\/+$/,"")}/attachments/`}export function inboxAttachmentId(key){return createHash("sha256").update(key).digest("base64url").slice(0,22)}export function inboxAttachmentName(key,prefix){const storedName=(key.startsWith(prefix)?key.slice(prefix.length):key.split("/").pop()||"").replace(/^stacks-\d{4}--/,"");let decodedName=storedName;try{decodedName=decodeURIComponent(storedName)}catch{}return decodedName.replace(/[\u0000-\u001F\u007F]/g,"").replace(/[\\/]/g,"_").trim().slice(0,240)||"attachment"}export function mapInboxAttachmentObjects(basePath,objects){const prefix=inboxAttachmentPrefix(basePath);return objects.filter((object)=>object.Key.startsWith(prefix)&&object.Key.length>prefix.length&&!object.Key.endsWith("/")).map((object)=>({id:inboxAttachmentId(object.Key),key:object.Key,name:inboxAttachmentName(object.Key,prefix),size:Math.max(0,Number(object.Size)||0),...object.LastModified?{lastModified:object.LastModified}:{}})).sort((left,right)=>left.name.localeCompare(right.name))}export function inboxAttachmentContentDisposition(filename){const asciiName=filename.normalize("NFKD").replace(/[^\x20-\x7E]/g,"").replace(/["\\]/g,"_").trim()||"attachment",utf8Name=encodeURIComponent(filename).replace(/['()]/g,(character)=>`%${character.charCodeAt(0).toString(16).toUpperCase()}`);return`attachment; filename="${asciiName}"; filename*=UTF-8''${utf8Name}`}export function inboxAttachmentContentType(contentType){const mediaType=contentType?.split(";",1)[0]?.trim().toLowerCase()||"";return/^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/.test(mediaType)?mediaType:"application/octet-stream"}
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type InboxAttachment } from './inbox-attachments';
|
|
1
2
|
// Export singleton instance
|
|
2
3
|
export declare const emailSDK: EmailSDK;
|
|
3
4
|
// Export convenience functions
|
|
@@ -60,11 +61,19 @@ export declare interface SendResult {
|
|
|
60
61
|
messageId?: string
|
|
61
62
|
error?: string
|
|
62
63
|
}
|
|
64
|
+
export declare interface EmailSDKOptions {
|
|
65
|
+
bucket?: string
|
|
66
|
+
region?: string
|
|
67
|
+
domain?: string
|
|
68
|
+
storage?: () => EmailStorageClient | Promise<EmailStorageClient>
|
|
69
|
+
}
|
|
70
|
+
export type EmailStorageClient = Pick<import('@stacksjs/ts-cloud').S3Client,
|
|
71
|
+
'deleteObjects' | 'getObject' | 'getObjectBytes' | 'listObjects' | 'putObject'>;
|
|
63
72
|
/**
|
|
64
73
|
* Email SDK class for Stacks applications
|
|
65
74
|
*/
|
|
66
75
|
export declare class EmailSDK {
|
|
67
|
-
constructor(options?:
|
|
76
|
+
constructor(options?: EmailSDKOptions);
|
|
68
77
|
send(message: EmailMessage): Promise<SendResult>;
|
|
69
78
|
sendTemplate(options: {
|
|
70
79
|
to: string | string[]
|
|
@@ -81,10 +90,18 @@ export declare class EmailSDK {
|
|
|
81
90
|
html?: string
|
|
82
91
|
text?: string
|
|
83
92
|
raw?: string
|
|
93
|
+
attachments: InboxAttachment[]
|
|
94
|
+
} | null>;
|
|
95
|
+
getAttachments(mailbox: string, messageId: string): Promise<InboxAttachment[] | null>;
|
|
96
|
+
getAttachment(mailbox: string, messageId: string, attachmentId: string): Promise<{
|
|
97
|
+
attachment: InboxAttachment
|
|
98
|
+
body: Uint8Array
|
|
99
|
+
contentType: string
|
|
84
100
|
} | null>;
|
|
85
101
|
search(mailbox: string, options: EmailSearchOptions): Promise<InboxEmail[]>;
|
|
86
102
|
delete(mailbox: string, messageId: string): Promise<boolean>;
|
|
87
103
|
markAsRead(mailbox: string, messageId: string): Promise<boolean>;
|
|
88
104
|
markAsUnread(mailbox: string, messageId: string): Promise<boolean>;
|
|
89
105
|
}
|
|
106
|
+
export * from './inbox-attachments';
|
|
90
107
|
export default EmailSDK;
|
package/dist/sdk/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{email as emailConfig}from"@stacksjs/config";import{getErrorMessage}from"@stacksjs/utils";export class EmailSDK{bucket;region;domain;constructor(options){const appName=(process.env.APP_NAME||"stacks").toLowerCase().replace(/[^a-z0-9-]/g,"-");this.bucket=options?.bucket||process.env.AWS_BUCKET||`${appName}-production-email`;this.region=options?.region||process.env.AWS_REGION||"us-east-1";const fromAddress=emailConfig?.from?.address,parsedDomain=fromAddress?.includes("@")?fromAddress.split("@")[1]:void 0;this.domain=options?.domain||parsedDomain||"stacksjs.com"}async send(message){try{const{SESClient}=await import("@stacksjs/ts-cloud"),ses=new SESClient(this.region),from=this.normalizeAddress(message.from||emailConfig?.from||{address:`noreply@${this.domain}`}),toAddresses=this.normalizeAddresses(message.to),ccAddresses=message.cc?this.normalizeAddresses(message.cc):void 0,bccAddresses=message.bcc?this.normalizeAddresses(message.bcc):void 0;return{success:!0,messageId:(await ses.sendEmail({FromEmailAddress:typeof from==="string"?from:`${from.name} <${from.address}>`,Destination:{ToAddresses:toAddresses,CcAddresses:ccAddresses,BccAddresses:bccAddresses},ReplyToAddresses:message.replyTo?[typeof message.replyTo==="string"?message.replyTo:message.replyTo.address]:void 0,Content:{Simple:{Subject:{Data:message.subject,Charset:"UTF-8"},Body:{...message.html&&{Html:{Data:message.html,Charset:"UTF-8"}},...message.text&&{Text:{Data:message.text,Charset:"UTF-8"}}}}}})).MessageId}}catch(error){return{success:!1,error:getErrorMessage(error)}}}async sendTemplate(options){const html=this.renderTemplate(options.template,options.data),subject=options.subject||options.template;return this.send({to:options.to,from:options.from,subject,html})}async getInbox(mailbox,options){try{const{S3Client}=await import("@stacksjs/ts-cloud"),s3=new S3Client(this.region),[localPart,domain]=mailbox.includes("@")?mailbox.split("@"):[mailbox,this.domain],indexKey=`mailboxes/${domain}/${localPart}/inbox.json`,result=await s3.getObject(this.bucket,indexKey);if(!result)return[];let inbox=JSON.parse(result);const offset=options?.offset||0,limit=options?.limit||50;return inbox.slice(offset,offset+limit)}catch(error){if(getErrorMessage(error).includes("NoSuchKey")||getErrorMessage(error).includes("404"))return[];throw error}}async getInboxStats(mailbox){const inbox=await this.getInbox(mailbox,{limit:1000}),unread=inbox.filter((e)=>!e.read).length;return{total:inbox.length,unread,read:inbox.length-unread}}async getUnreadCount(mailbox){return(await this.getInboxStats(mailbox)).unread}async getEmail(mailbox,messageId){try{const{S3Client}=await import("@stacksjs/ts-cloud"),s3=new S3Client(this.region),[localPart,domain]=mailbox.includes("@")?mailbox.split("@"):[mailbox,this.domain],email=(await this.getInbox(mailbox,{limit:1000})).find((e)=>e.messageId===messageId);if(!email)return null;const basePath=email.path,metaResult=await s3.getObject(this.bucket,`${basePath}/metadata.json`);let metadata={};if(metaResult)try{metadata=JSON.parse(metaResult)}catch(parseError){console.debug(`[email-sdk] Failed to parse email metadata: ${parseError.message}`)}let html;try{html=await s3.getObject(this.bucket,`${basePath}/body.html`)||void 0}catch(error){if(!getErrorMessage(error)?.includes("NoSuchKey")&&!getErrorMessage(error)?.includes("404"))console.debug(`[email-sdk] Failed to fetch HTML body: ${getErrorMessage(error)}`)}let text;try{text=await s3.getObject(this.bucket,`${basePath}/body.txt`)||void 0}catch(error){if(!getErrorMessage(error)?.includes("NoSuchKey")&&!getErrorMessage(error)?.includes("404"))console.debug(`[email-sdk] Failed to fetch text body: ${getErrorMessage(error)}`)}return{metadata,html,text}}catch(error){if(getErrorMessage(error).includes("NoSuchKey")||getErrorMessage(error).includes("404"))return null;throw error}}async search(mailbox,options){let results=await this.getInbox(mailbox,{limit:1000});if(options.from){const fromLower=options.from.toLowerCase();results=results.filter((e)=>e.from.toLowerCase().includes(fromLower))}if(options.subject){const subjectLower=options.subject.toLowerCase();results=results.filter((e)=>e.subject.toLowerCase().includes(subjectLower))}if(options.after)results=results.filter((e)=>new Date(e.date)>=options.after);if(options.before)results=results.filter((e)=>new Date(e.date)<=options.before);if(options.hasAttachments!==void 0)results=results.filter((e)=>e.hasAttachments===options.hasAttachments);const offset=options.offset||0,limit=options.limit||50;return results.slice(offset,offset+limit)}async delete(mailbox,messageId){const{S3Client}=await import("@stacksjs/ts-cloud"),s3=new S3Client(this.region),[localPart,domain]=mailbox.includes("@")?mailbox.split("@"):[mailbox,this.domain],inbox=await this.getInbox(mailbox,{limit:1000}),emailIndex=inbox.findIndex((e)=>e.messageId===messageId);if(emailIndex===-1)return!1;const email=inbox[emailIndex];if(!email)return!1;const basePath=email.path,keysToDelete=[`${basePath}/metadata.json`,`${basePath}/raw.eml`,`${basePath}/body.html`,`${basePath}/body.txt`,`${basePath}/preview.txt`];for(const key of keysToDelete)try{await s3.deleteObject(this.bucket,key)}catch(error){if(!getErrorMessage(error)?.includes("NoSuchKey")&&!getErrorMessage(error)?.includes("404"))throw error}inbox.splice(emailIndex,1);await s3.putObject({bucket:this.bucket,key:`mailboxes/${domain}/${localPart}/inbox.json`,body:JSON.stringify(inbox,null,2),contentType:"application/json"});return!0}async markAsRead(mailbox,messageId){return this.updateEmailStatus(mailbox,messageId,{read:!0})}async markAsUnread(mailbox,messageId){return this.updateEmailStatus(mailbox,messageId,{read:!1})}async updateEmailStatus(mailbox,messageId,updates){const{S3Client}=await import("@stacksjs/ts-cloud"),s3=new S3Client(this.region),[localPart,domain]=mailbox.includes("@")?mailbox.split("@"):[mailbox,this.domain],inbox=await this.getInbox(mailbox,{limit:1000}),emailIndex=inbox.findIndex((e)=>e.messageId===messageId);if(emailIndex===-1)return!1;Object.assign(inbox[emailIndex],updates);await s3.putObject({bucket:this.bucket,key:`mailboxes/${domain}/${localPart}/inbox.json`,body:JSON.stringify(inbox,null,2),contentType:"application/json"});return!0}normalizeAddress(addr){if(typeof addr==="string"){const match=addr.match(/^(.+?)\s*<(.+)>$/);if(match)return{name:match[1].trim(),address:match[2].trim()};return{address:addr}}return addr}normalizeAddresses(addrs){return(Array.isArray(addrs)?addrs:[addrs]).map((a)=>{if(typeof a==="string")return a;return a.name?`${a.name} <${a.address}>`:a.address})}renderTemplate(template,data){let result=template;for(const[key,value]of Object.entries(data)){const escapedKey=key.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");result=result.replace(new RegExp(`{{\\s*${escapedKey}\\s*}}`,"g"),String(value))}return result}}export const emailSDK=new EmailSDK,sendEmail=(message)=>emailSDK.send(message),getInbox=(mailbox,options)=>emailSDK.getInbox(mailbox,options),getInboxStats=(mailbox)=>emailSDK.getInboxStats(mailbox),getUnreadCount=(mailbox)=>emailSDK.getUnreadCount(mailbox),searchEmails=(mailbox,options)=>emailSDK.search(mailbox,options),deleteEmail=(mailbox,messageId)=>emailSDK.delete(mailbox,messageId);export default EmailSDK;
|
|
1
|
+
import{email as emailConfig}from"@stacksjs/config";import{getErrorMessage}from"@stacksjs/utils";import{inboxMailboxPath,inboxMessagePath}from"../inbox-mailbox";import{normalizeEmailHtmlBody,normalizeEmailTextBody}from"../mime-preview";import{inboxAttachmentPrefix,mapInboxAttachmentObjects}from"./inbox-attachments";export*from"./inbox-attachments";function inboxString(value,field,index){if(typeof value!=="string")throw TypeError(`Inbox entry ${index} has an invalid ${field}.`);const normalized=value.trim();if(!normalized||/[\u0000-\u001F\u007F]/.test(normalized))throw TypeError(`Inbox entry ${index} has an invalid ${field}.`);return normalized}function normalizeInboxEntry(value,index,mailbox){if(!value||typeof value!=="object"||Array.isArray(value))throw TypeError(`Inbox entry ${index} must be an object.`);const entry=value,messageId=inboxString(entry.messageId,"messageId",index);if(messageId.length>512)throw TypeError(`Inbox entry ${index} has an invalid messageId.`);const path=inboxMessagePath(inboxString(entry.path,"path",index),mailbox);return{messageId,from:inboxString(entry.from,"from",index),...typeof entry.fromName==="string"&&entry.fromName.trim()?{fromName:entry.fromName.trim()}:{},to:inboxString(entry.to,"to",index),subject:typeof entry.subject==="string"?entry.subject:"",date:inboxString(entry.date,"date",index),read:entry.read===!0,...typeof entry.preview==="string"?{preview:entry.preview}:{},hasAttachments:entry.hasAttachments===!0,path}}export class EmailSDK{bucket;region;domain;storageFactory;constructor(options){const appName=(process.env.APP_NAME||"stacks").toLowerCase().replace(/[^a-z0-9-]/g,"-");this.bucket=options?.bucket||process.env.AWS_BUCKET||`${appName}-production-email`;this.region=options?.region||process.env.AWS_REGION||"us-east-1";const fromAddress=emailConfig?.from?.address,parsedDomain=fromAddress?.includes("@")?fromAddress.split("@")[1]:void 0;this.domain=options?.domain||emailConfig?.domain||parsedDomain||"stacksjs.com";this.storageFactory=options?.storage}async send(message){try{const{SESClient}=await import("@stacksjs/ts-cloud"),ses=new SESClient(this.region),from=this.normalizeAddress(message.from||emailConfig?.from||{address:`noreply@${this.domain}`}),toAddresses=this.normalizeAddresses(message.to),ccAddresses=message.cc?this.normalizeAddresses(message.cc):void 0,bccAddresses=message.bcc?this.normalizeAddresses(message.bcc):void 0;return{success:!0,messageId:(await ses.sendEmail({FromEmailAddress:typeof from==="string"?from:`${from.name} <${from.address}>`,Destination:{ToAddresses:toAddresses,CcAddresses:ccAddresses,BccAddresses:bccAddresses},ReplyToAddresses:message.replyTo?[typeof message.replyTo==="string"?message.replyTo:message.replyTo.address]:void 0,Content:{Simple:{Subject:{Data:message.subject,Charset:"UTF-8"},Body:{...message.html&&{Html:{Data:message.html,Charset:"UTF-8"}},...message.text&&{Text:{Data:message.text,Charset:"UTF-8"}}}}}})).MessageId}}catch(error){return{success:!1,error:getErrorMessage(error)}}}async sendTemplate(options){const html=this.renderTemplate(options.template,options.data),subject=options.subject||options.template;return this.send({to:options.to,from:options.from,subject,html})}async getInbox(mailbox,options){try{const mailboxPath=inboxMailboxPath(mailbox,this.domain),result=await(await this.storage()).getObject(this.bucket,mailboxPath.indexKey);if(!result)return[];const parsed=JSON.parse(result);if(!Array.isArray(parsed))throw TypeError("Inbox index must be a JSON array.");const inbox=parsed.map((entry,index)=>normalizeInboxEntry(entry,index,mailboxPath)),offset=Math.max(0,Math.trunc(Number(options?.offset)||0)),limit=Math.min(1000,Math.max(1,Math.trunc(Number(options?.limit)||50)));return inbox.slice(offset,offset+limit)}catch(error){if(getErrorMessage(error).includes("NoSuchKey")||getErrorMessage(error).includes("404"))return[];throw error}}async getInboxStats(mailbox){const inbox=await this.getInbox(mailbox,{limit:1000}),unread=inbox.filter((e)=>!e.read).length;return{total:inbox.length,unread,read:inbox.length-unread}}async getUnreadCount(mailbox){return(await this.getInboxStats(mailbox)).unread}async getEmail(mailbox,messageId){try{const email=(await this.getInbox(mailbox,{limit:1000})).find((e)=>e.messageId===messageId);if(!email)return null;const s3=await this.storage(),basePath=email.path,metaResult=await s3.getObject(this.bucket,`${basePath}/metadata.json`);let metadata={};if(metaResult)try{metadata=JSON.parse(metaResult)}catch(parseError){console.debug(`[email-sdk] Failed to parse email metadata: ${parseError.message}`)}let html;try{const htmlResult=await s3.getObject(this.bucket,`${basePath}/body.html`);html=htmlResult?normalizeEmailHtmlBody(htmlResult):void 0}catch(error){if(!getErrorMessage(error)?.includes("NoSuchKey")&&!getErrorMessage(error)?.includes("404"))console.debug(`[email-sdk] Failed to fetch HTML body: ${getErrorMessage(error)}`)}let text;try{const textResult=await s3.getObject(this.bucket,`${basePath}/body.txt`);text=textResult?normalizeEmailTextBody(textResult):void 0}catch(error){if(!getErrorMessage(error)?.includes("NoSuchKey")&&!getErrorMessage(error)?.includes("404"))console.debug(`[email-sdk] Failed to fetch text body: ${getErrorMessage(error)}`)}const attachments=await this.listAttachments(s3,email);return{metadata,html,text,attachments}}catch(error){if(getErrorMessage(error).includes("NoSuchKey")||getErrorMessage(error).includes("404"))return null;throw error}}async getAttachments(mailbox,messageId){const email=(await this.getInbox(mailbox,{limit:1000})).find((entry)=>entry.messageId===messageId);if(!email)return null;return this.listAttachments(await this.storage(),email)}async getAttachment(mailbox,messageId,attachmentId){const email=(await this.getInbox(mailbox,{limit:1000})).find((entry)=>entry.messageId===messageId);if(!email)return null;const s3=await this.storage(),attachment=(await this.listStoredAttachments(s3,email)).find((item)=>item.id===attachmentId);if(!attachment)return null;const result=await s3.getObjectBytes(this.bucket,attachment.key);return{attachment:{id:attachment.id,name:attachment.name,size:attachment.size||result.contentLength||result.body.byteLength,...attachment.lastModified?{lastModified:attachment.lastModified}:{}},body:result.body,contentType:result.contentType||"application/octet-stream"}}async search(mailbox,options){let results=await this.getInbox(mailbox,{limit:1000});if(options.from){const fromLower=options.from.toLowerCase();results=results.filter((e)=>e.from.toLowerCase().includes(fromLower))}if(options.subject){const subjectLower=options.subject.toLowerCase();results=results.filter((e)=>e.subject.toLowerCase().includes(subjectLower))}if(options.after)results=results.filter((e)=>new Date(e.date)>=options.after);if(options.before)results=results.filter((e)=>new Date(e.date)<=options.before);if(options.hasAttachments!==void 0)results=results.filter((e)=>e.hasAttachments===options.hasAttachments);const offset=options.offset||0,limit=options.limit||50;return results.slice(offset,offset+limit)}async delete(mailbox,messageId){const mailboxPath=inboxMailboxPath(mailbox,this.domain),s3=await this.storage(),inbox=await this.getInbox(mailbox,{limit:1000}),emailIndex=inbox.findIndex((e)=>e.messageId===messageId);if(emailIndex===-1)return!1;const email=inbox[emailIndex];if(!email)return!1;const basePath=email.path,keysToDelete=await this.listStoredObjectKeys(s3,`${basePath}/`);for(let offset=0;offset<keysToDelete.length;offset+=1000)await s3.deleteObjects(this.bucket,keysToDelete.slice(offset,offset+1000));inbox.splice(emailIndex,1);await s3.putObject({bucket:this.bucket,key:mailboxPath.indexKey,body:JSON.stringify(inbox,null,2),contentType:"application/json"});return!0}async listAttachments(s3,email){return(await this.listStoredAttachments(s3,email)).map((attachment)=>({id:attachment.id,name:attachment.name,size:attachment.size,...attachment.lastModified?{lastModified:attachment.lastModified}:{}}))}async listStoredAttachments(s3,email){const result=await s3.listObjects({bucket:this.bucket,prefix:inboxAttachmentPrefix(email.path),maxKeys:1000});return mapInboxAttachmentObjects(email.path,result.objects)}async listStoredObjectKeys(s3,prefix){const keys=[];let continuationToken;do{const result=await s3.listObjects({bucket:this.bucket,prefix,maxKeys:1000,...continuationToken?{continuationToken}:{}});keys.push(...result.objects.map((object)=>object.Key).filter(Boolean));continuationToken=result.nextContinuationToken}while(continuationToken);return keys}async markAsRead(mailbox,messageId){return this.updateEmailStatus(mailbox,messageId,{read:!0})}async markAsUnread(mailbox,messageId){return this.updateEmailStatus(mailbox,messageId,{read:!1})}async updateEmailStatus(mailbox,messageId,updates){const mailboxPath=inboxMailboxPath(mailbox,this.domain),s3=await this.storage(),inbox=await this.getInbox(mailbox,{limit:1000}),emailIndex=inbox.findIndex((e)=>e.messageId===messageId);if(emailIndex===-1)return!1;Object.assign(inbox[emailIndex],updates);await s3.putObject({bucket:this.bucket,key:mailboxPath.indexKey,body:JSON.stringify(inbox,null,2),contentType:"application/json"});return!0}async storage(){if(this.storageFactory)return this.storageFactory();const{S3Client}=await import("@stacksjs/ts-cloud");return new S3Client(this.region)}normalizeAddress(addr){if(typeof addr==="string"){const match=addr.match(/^(.+?)\s*<(.+)>$/);if(match)return{name:match[1].trim(),address:match[2].trim()};return{address:addr}}return addr}normalizeAddresses(addrs){return(Array.isArray(addrs)?addrs:[addrs]).map((a)=>{if(typeof a==="string")return a;return a.name?`${a.name} <${a.address}>`:a.address})}renderTemplate(template,data){let result=template;for(const[key,value]of Object.entries(data)){const escapedKey=key.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");result=result.replace(new RegExp(`{{\\s*${escapedKey}\\s*}}`,"g"),String(value))}return result}}export const emailSDK=new EmailSDK,sendEmail=(message)=>emailSDK.send(message),getInbox=(mailbox,options)=>emailSDK.getInbox(mailbox,options),getInboxStats=(mailbox)=>emailSDK.getInboxStats(mailbox),getUnreadCount=(mailbox)=>emailSDK.getUnreadCount(mailbox),searchEmails=(mailbox,options)=>emailSDK.search(mailbox,options),deleteEmail=(mailbox,messageId)=>emailSDK.delete(mailbox,messageId);export default EmailSDK;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/email",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.70.
|
|
5
|
+
"version": "0.70.355",
|
|
6
6
|
"description": "The Stacks Email integration. Painlessly create & manage your inboxes, templates, and send emails.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -60,13 +60,14 @@
|
|
|
60
60
|
"prepublishOnly": "bun run build"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@stacksjs/ts-cloud": "^0.7.103"
|
|
63
|
+
"@stacksjs/ts-cloud": "^0.7.103",
|
|
64
|
+
"postal-mime": "^2.7.6"
|
|
64
65
|
},
|
|
65
66
|
"devDependencies": {
|
|
66
|
-
"@stacksjs/cli": "0.70.
|
|
67
|
-
"@stacksjs/config": "0.70.
|
|
67
|
+
"@stacksjs/cli": "0.70.355",
|
|
68
|
+
"@stacksjs/config": "0.70.355",
|
|
68
69
|
"better-dx": "^0.2.17",
|
|
69
|
-
"@stacksjs/error-handling": "0.70.
|
|
70
|
-
"@stacksjs/types": "0.70.
|
|
70
|
+
"@stacksjs/error-handling": "0.70.355",
|
|
71
|
+
"@stacksjs/types": "0.70.355"
|
|
71
72
|
}
|
|
72
73
|
}
|