@stacksjs/email 0.70.88 → 0.70.91

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.
Files changed (65) hide show
  1. package/dist/css-inliner.d.ts +26 -0
  2. package/dist/css-inliner.js +172 -0
  3. package/dist/drivers/base.d.ts +14 -0
  4. package/dist/drivers/base.js +128 -0
  5. package/dist/drivers/capture.d.ts +55 -0
  6. package/dist/drivers/capture.js +30 -0
  7. package/dist/drivers/index.d.ts +10 -0
  8. package/dist/drivers/index.js +6 -0
  9. package/dist/drivers/log.d.ts +30 -0
  10. package/dist/drivers/log.js +81 -0
  11. package/dist/drivers/mailgun.d.ts +8 -0
  12. package/dist/drivers/mailgun.js +127 -0
  13. package/dist/drivers/mailtrap.d.ts +8 -0
  14. package/dist/drivers/mailtrap.js +129 -0
  15. package/dist/drivers/sendgrid.d.ts +8 -0
  16. package/dist/drivers/sendgrid.js +136 -0
  17. package/dist/drivers/ses.d.ts +8 -0
  18. package/dist/drivers/ses.js +113 -0
  19. package/dist/drivers/smtp.d.ts +14 -0
  20. package/dist/drivers/smtp.js +225 -0
  21. package/dist/email.d.ts +39 -0
  22. package/dist/email.js +198 -0
  23. package/dist/idempotency.d.ts +20 -0
  24. package/dist/idempotency.js +56 -0
  25. package/dist/index.d.ts +22 -0
  26. package/dist/index.js +19 -0
  27. package/dist/mailable.d.ts +116 -0
  28. package/dist/mailable.js +145 -0
  29. package/dist/mime.d.ts +37 -0
  30. package/dist/mime.js +89 -0
  31. package/dist/preview-ui.d.ts +11 -0
  32. package/dist/preview-ui.js +132 -0
  33. package/dist/preview.d.ts +50 -0
  34. package/dist/preview.js +97 -0
  35. package/dist/sdk/index.d.ts +81 -0
  36. package/dist/sdk/index.js +219 -0
  37. package/dist/send.d.ts +1 -0
  38. package/dist/send.js +0 -0
  39. package/dist/server/converter.d.ts +1 -0
  40. package/dist/server/converter.js +0 -0
  41. package/dist/server/inbound.d.ts +1 -0
  42. package/dist/server/inbound.js +0 -0
  43. package/dist/server/outbound.d.ts +1 -0
  44. package/dist/server/outbound.js +0 -0
  45. package/dist/suppression.d.ts +82 -0
  46. package/dist/suppression.js +104 -0
  47. package/dist/template.d.ts +99 -0
  48. package/dist/template.js +170 -0
  49. package/dist/types.d.ts +37 -0
  50. package/dist/types.js +0 -0
  51. package/dist/unsubscribe.d.ts +39 -0
  52. package/dist/unsubscribe.js +65 -0
  53. package/dist/utils/config.d.ts +3 -0
  54. package/dist/utils/config.js +3 -0
  55. package/dist/validation.d.ts +48 -0
  56. package/dist/validation.js +22 -0
  57. package/dist/webhook-dedup.d.ts +10 -0
  58. package/dist/webhook-dedup.js +33 -0
  59. package/dist/webhook-events.d.ts +34 -0
  60. package/dist/webhook-events.js +37 -0
  61. package/dist/webhook-handlers.d.ts +27 -0
  62. package/dist/webhook-handlers.js +264 -0
  63. package/dist/webhook-signatures.d.ts +91 -0
  64. package/dist/webhook-signatures.js +148 -0
  65. package/package.json +5 -5
@@ -0,0 +1,127 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { config } from "@stacksjs/config";
3
+ import { log } from "@stacksjs/logging";
4
+ import { template } from "../template";
5
+ import { BaseEmailDriver } from "./base";
6
+
7
+ export class MailgunDriver extends BaseEmailDriver {
8
+ name = "mailgun";
9
+ apiKey = null;
10
+ domain = null;
11
+ endpoint = null;
12
+ getConfig() {
13
+ if (!this.apiKey || !this.domain || !this.endpoint) {
14
+ this.apiKey = config.services.mailgun?.apiKey ?? "";
15
+ this.domain = config.services.mailgun?.domain ?? "";
16
+ this.endpoint = config.services.mailgun?.endpoint ?? "api.mailgun.net";
17
+ }
18
+ return {
19
+ apiKey: this.apiKey,
20
+ domain: this.domain,
21
+ endpoint: this.endpoint
22
+ };
23
+ }
24
+ async send(message, options) {
25
+ const { domain } = this.getConfig(), logContext = {
26
+ provider: this.name,
27
+ to: message.to,
28
+ subject: message.subject,
29
+ domain
30
+ };
31
+ log.info("Sending email via Mailgun...", logContext);
32
+ try {
33
+ this.validateMessage(message);
34
+ let htmlContent;
35
+ if (message.template) {
36
+ const templ = await template(message.template, options);
37
+ if (templ && "html" in templ)
38
+ htmlContent = templ.html;
39
+ }
40
+ const finalHtml = htmlContent || message.html, formData = new FormData, fromAddress = {
41
+ address: message.from?.address || config.email.from?.address || "",
42
+ name: message.from?.name || config.email.from?.name
43
+ };
44
+ formData.append("from", this.formatMailgunAddress(fromAddress));
45
+ this.formatMailgunAddresses(message.to).forEach((to) => formData.append("to", to));
46
+ if (message.cc)
47
+ this.formatMailgunAddresses(message.cc).forEach((cc) => formData.append("cc", cc));
48
+ if (message.bcc)
49
+ this.formatMailgunAddresses(message.bcc).forEach((bcc) => formData.append("bcc", bcc));
50
+ formData.append("subject", message.subject);
51
+ if (message.replyTo) {
52
+ const formatted = this.formatMailgunAddresses(Array.isArray(message.replyTo) || typeof message.replyTo === "string" ? message.replyTo : [message.replyTo]);
53
+ if (formatted.length > 0)
54
+ formData.append("h:Reply-To", formatted.join(", "));
55
+ }
56
+ if (message.headers) {
57
+ for (const [k, v] of Object.entries(message.headers))
58
+ if (typeof v === "string")
59
+ formData.append(`h:${k}`, v);
60
+ }
61
+ if (finalHtml)
62
+ formData.append("html", finalHtml);
63
+ if (message.text)
64
+ formData.append("text", message.text);
65
+ if (message.attachments)
66
+ message.attachments.forEach((attachment) => {
67
+ const content = typeof attachment.content === "string" ? attachment.content : this.arrayBufferToBase64(attachment.content);
68
+ formData.append("attachment", new Blob([content], { type: attachment.contentType }), attachment.filename);
69
+ });
70
+ const response = await this.sendWithRetry(formData);
71
+ return this.handleSuccess(message, response.id);
72
+ } catch (error) {
73
+ return this.handleError(error, message);
74
+ }
75
+ }
76
+ formatMailgunAddress(address) {
77
+ return address.name ? `${address.name} <${address.address}>` : address.address;
78
+ }
79
+ formatMailgunAddresses(addresses) {
80
+ if (!addresses)
81
+ return [];
82
+ if (typeof addresses === "string")
83
+ return [addresses];
84
+ return addresses.map((_addr) => {
85
+ if (typeof _addr === "string")
86
+ return _addr;
87
+ if (!_addr.name)
88
+ return _addr.address;
89
+ return `${/[",()<>[\]:;@\\]/.test(_addr.name) ? `"${_addr.name.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"` : _addr.name} <${_addr.address}>`;
90
+ });
91
+ }
92
+ arrayBufferToBase64(buffer) {
93
+ let binary = "";
94
+ const bytes = new Uint8Array(buffer), len = bytes.byteLength;
95
+ for (let i = 0;i < len; i++)
96
+ binary += String.fromCharCode(bytes[i] ?? 0);
97
+ return typeof btoa === "function" ? btoa(binary) : Buffer.from(binary).toString("base64");
98
+ }
99
+ async sendWithRetry(formData, attempt = 1) {
100
+ const { apiKey, domain, endpoint } = this.getConfig(), url = `https://${endpoint}/v3/${domain}/messages`, auth = Buffer.from(`api:${apiKey}`).toString("base64");
101
+ try {
102
+ const response = await fetch(url, {
103
+ method: "POST",
104
+ headers: {
105
+ Authorization: `Basic ${auth}`
106
+ },
107
+ body: formData
108
+ });
109
+ if (!response.ok) {
110
+ const errorData = await response.json();
111
+ throw Error(`Mailgun API error: ${response.status} - ${JSON.stringify(errorData)}`);
112
+ }
113
+ const data = await response.json();
114
+ log.info(`[${this.name}] Email sent successfully`, { attempt, messageId: data.id });
115
+ return data;
116
+ } catch (error) {
117
+ if (attempt < (config.services.mailgun?.maxRetries ?? 3)) {
118
+ const retryTimeout = config.services.mailgun?.retryTimeout ?? 1000;
119
+ log.warn(`[${this.name}] Email send failed, retrying (${attempt}/${config.services.mailgun?.maxRetries ?? 3})`);
120
+ await new Promise((resolve) => setTimeout(resolve, retryTimeout));
121
+ return this.sendWithRetry(formData, attempt + 1);
122
+ }
123
+ throw error;
124
+ }
125
+ }
126
+ }
127
+ export default MailgunDriver;
@@ -0,0 +1,8 @@
1
+ import { BaseEmailDriver } from './base';
2
+ import type { EmailMessage, EmailResult } from '@stacksjs/types';
3
+ import type { TemplateOptions } from '../template';
4
+ export declare class MailtrapDriver extends BaseEmailDriver {
5
+ name: string;
6
+ send(message: EmailMessage, options?: TemplateOptions): Promise<EmailResult>;
7
+ }
8
+ export default MailtrapDriver;
@@ -0,0 +1,129 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { config } from "@stacksjs/config";
3
+ import { log } from "@stacksjs/logging";
4
+ import { template } from "../template";
5
+ import { filterStringHeaders } from "../validation";
6
+ import { BaseEmailDriver } from "./base";
7
+
8
+ export class MailtrapDriver extends BaseEmailDriver {
9
+ name = "mailtrap";
10
+ host = null;
11
+ token = null;
12
+ inboxId = null;
13
+ getConfig() {
14
+ if (this.host === null || this.token === null || this.inboxId === null) {
15
+ this.host = config.services.mailtrap?.host ?? "https://sandbox.api.mailtrap.io/api/send";
16
+ this.token = config.services.mailtrap?.token ?? "";
17
+ this.inboxId = config.services.mailtrap?.inboxId ? Number(config.services.mailtrap.inboxId) : void 0;
18
+ }
19
+ return {
20
+ host: this.host,
21
+ token: this.token,
22
+ inboxId: this.inboxId
23
+ };
24
+ }
25
+ async send(message, options) {
26
+ const { inboxId } = this.getConfig(), logContext = {
27
+ provider: this.name,
28
+ to: message.to,
29
+ subject: message.subject,
30
+ inboxId
31
+ };
32
+ log.info("Sending email via Mailtrap...", logContext);
33
+ try {
34
+ this.validateMessage(message);
35
+ let templ;
36
+ if (message.template)
37
+ templ = await template(message.template, options);
38
+ const htmlContent = templ?.html || message.html, replyTo = this.firstMailtrapAddress(message.replyTo), customHeaders = filterStringHeaders(message.headers), mailtrapPayload = {
39
+ from: {
40
+ email: message.from?.address || config.email.from?.address || "",
41
+ name: message.from?.name || config.email.from?.name
42
+ },
43
+ to: this.formatMailtrapAddresses(message.to),
44
+ ...message.cc && { cc: this.formatMailtrapAddresses(message.cc) },
45
+ ...message.bcc && { bcc: this.formatMailtrapAddresses(message.bcc) },
46
+ ...replyTo ? { reply_to: replyTo } : {},
47
+ ...customHeaders ? { headers: customHeaders } : {},
48
+ subject: message.subject,
49
+ ...htmlContent && { html: htmlContent },
50
+ ...message.text && { text: message.text },
51
+ ...message.attachments && {
52
+ attachments: message.attachments.map((attachment) => ({
53
+ filename: attachment.filename,
54
+ content: typeof attachment.content === "string" ? attachment.content : this.arrayBufferToBase64(attachment.content),
55
+ type: attachment.contentType || "application/octet-stream"
56
+ }))
57
+ }
58
+ }, response = await this.sendWithRetry(mailtrapPayload);
59
+ return this.handleSuccess(message, response.message_ids?.[0]);
60
+ } catch (error) {
61
+ return this.handleError(error, message);
62
+ }
63
+ }
64
+ formatMailtrapAddresses(addresses) {
65
+ if (!addresses)
66
+ return [];
67
+ if (typeof addresses === "string")
68
+ return [{ email: addresses }];
69
+ return addresses.map((addr) => {
70
+ if (typeof addr === "string")
71
+ return { email: addr };
72
+ return { email: addr.address, ...addr.name && { name: addr.name } };
73
+ });
74
+ }
75
+ firstMailtrapAddress(value) {
76
+ if (!value)
77
+ return;
78
+ if (typeof value === "string")
79
+ return { email: value };
80
+ if (Array.isArray(value)) {
81
+ const first = value[0];
82
+ if (first === void 0)
83
+ return;
84
+ if (typeof first === "string")
85
+ return { email: first };
86
+ return { email: first.address, ...first.name && { name: first.name } };
87
+ }
88
+ return { email: value.address, ...value.name && { name: value.name } };
89
+ }
90
+ arrayBufferToBase64(buffer) {
91
+ let binary = "";
92
+ const bytes = new Uint8Array(buffer), len = bytes.byteLength;
93
+ for (let i = 0;i < len; i++)
94
+ binary += String.fromCharCode(bytes[i] ?? 0);
95
+ return typeof btoa === "function" ? btoa(binary) : Buffer.from(binary).toString("base64");
96
+ }
97
+ async sendWithRetry(payload, attempt = 1) {
98
+ const { host, token, inboxId } = this.getConfig();
99
+ if (!inboxId)
100
+ throw Error("Mailtrap inbox ID is required but not provided. Please set MAILTRAP_INBOX_ID in your environment variables.");
101
+ const endpoint = `${host}/${inboxId}`;
102
+ try {
103
+ const response = await fetch(endpoint, {
104
+ method: "POST",
105
+ headers: {
106
+ Authorization: `Bearer ${token}`,
107
+ "Content-Type": "application/json"
108
+ },
109
+ body: JSON.stringify(payload)
110
+ });
111
+ if (!response.ok) {
112
+ const errorData = await response.json();
113
+ throw Error(`Mailtrap API error: ${response.status} - ${JSON.stringify(errorData)}`);
114
+ }
115
+ const data = await response.json();
116
+ log.info(`[${this.name}] Email sent successfully`, { attempt, messageId: data.message_ids?.[0] });
117
+ return data;
118
+ } catch (error) {
119
+ if (attempt < (config.services.mailtrap?.maxRetries ?? 3)) {
120
+ const retryTimeout = config.services.mailtrap?.retryTimeout ?? 1000;
121
+ log.warn(`[${this.name}] Email send failed, retrying (${attempt}/${config.services.mailtrap?.maxRetries ?? 3})`);
122
+ await new Promise((resolve) => setTimeout(resolve, retryTimeout));
123
+ return this.sendWithRetry(payload, attempt + 1);
124
+ }
125
+ throw error;
126
+ }
127
+ }
128
+ }
129
+ export default MailtrapDriver;
@@ -0,0 +1,8 @@
1
+ import { BaseEmailDriver } from './base';
2
+ import type { EmailMessage, EmailResult } from '@stacksjs/types';
3
+ import type { TemplateOptions } from '../template';
4
+ export declare class SendGridDriver extends BaseEmailDriver {
5
+ name: string;
6
+ send(message: EmailMessage, options?: TemplateOptions): Promise<EmailResult>;
7
+ }
8
+ export default SendGridDriver;
@@ -0,0 +1,136 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { config } from "@stacksjs/config";
3
+ import { log } from "@stacksjs/logging";
4
+ import { template } from "../template";
5
+ import { filterStringHeaders } from "../validation";
6
+ import { BaseEmailDriver } from "./base";
7
+
8
+ export class SendGridDriver extends BaseEmailDriver {
9
+ name = "sendgrid";
10
+ apiKey = null;
11
+ getApiKey() {
12
+ if (!this.apiKey)
13
+ this.apiKey = config.services.sendgrid?.apiKey ?? "";
14
+ return this.apiKey;
15
+ }
16
+ async send(message, options) {
17
+ const logContext = {
18
+ provider: this.name,
19
+ to: message.to,
20
+ subject: message.subject
21
+ };
22
+ log.info("Sending email via SendGrid...", logContext);
23
+ try {
24
+ this.validateMessage(message);
25
+ let htmlContent;
26
+ if (message.template) {
27
+ const templ = await template(message.template, options);
28
+ if (templ && "html" in templ)
29
+ htmlContent = templ.html;
30
+ }
31
+ const finalHtml = htmlContent || message.html, content = [];
32
+ if (finalHtml)
33
+ content.push({
34
+ type: "text/html",
35
+ value: finalHtml
36
+ });
37
+ if (message.text)
38
+ content.push({
39
+ type: "text/plain",
40
+ value: message.text
41
+ });
42
+ if (content.length === 0)
43
+ throw Error("Email must have either HTML or text content");
44
+ const replyTo = this.firstSendGridAddress(message.replyTo), customHeaders = filterStringHeaders(message.headers), sendgridPayload = {
45
+ personalizations: [
46
+ {
47
+ to: this.formatSendGridAddresses(message.to),
48
+ ...message.cc && { cc: this.formatSendGridAddresses(message.cc) },
49
+ ...message.bcc && { bcc: this.formatSendGridAddresses(message.bcc) },
50
+ subject: message.subject
51
+ }
52
+ ],
53
+ from: {
54
+ email: message.from?.address || config.email.from?.address || "",
55
+ name: message.from?.name || config.email.from?.name
56
+ },
57
+ ...replyTo ? { reply_to: replyTo } : {},
58
+ ...customHeaders ? { headers: customHeaders } : {},
59
+ content,
60
+ ...message.attachments && {
61
+ attachments: message.attachments.map((attachment) => ({
62
+ filename: attachment.filename,
63
+ content: typeof attachment.content === "string" ? attachment.content : this.arrayBufferToBase64(attachment.content),
64
+ type: attachment.contentType,
65
+ disposition: "attachment"
66
+ }))
67
+ }
68
+ }, response = await this.sendWithRetry(sendgridPayload);
69
+ return this.handleSuccess(message, response.headers?.get("x-message-id") ?? void 0);
70
+ } catch (error) {
71
+ return this.handleError(error, message);
72
+ }
73
+ }
74
+ formatSendGridAddresses(addresses) {
75
+ if (!addresses)
76
+ return [];
77
+ if (typeof addresses === "string")
78
+ return [{ email: addresses }];
79
+ return addresses.map((addr) => {
80
+ if (typeof addr === "string")
81
+ return { email: addr };
82
+ return { email: addr.address, ...addr.name && { name: addr.name } };
83
+ });
84
+ }
85
+ firstSendGridAddress(value) {
86
+ if (!value)
87
+ return;
88
+ if (typeof value === "string")
89
+ return { email: value };
90
+ if (Array.isArray(value)) {
91
+ const first = value[0];
92
+ if (first === void 0)
93
+ return;
94
+ if (typeof first === "string")
95
+ return { email: first };
96
+ return { email: first.address, ...first.name && { name: first.name } };
97
+ }
98
+ return { email: value.address, ...value.name && { name: value.name } };
99
+ }
100
+ arrayBufferToBase64(buffer) {
101
+ let binary = "";
102
+ const bytes = new Uint8Array(buffer), len = bytes.byteLength;
103
+ for (let i = 0;i < len; i++)
104
+ binary += String.fromCharCode(bytes[i] ?? 0);
105
+ return typeof btoa === "function" ? btoa(binary) : Buffer.from(binary).toString("base64");
106
+ }
107
+ async sendWithRetry(payload, attempt = 1) {
108
+ try {
109
+ const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
110
+ method: "POST",
111
+ headers: {
112
+ Authorization: `Bearer ${this.getApiKey()}`,
113
+ "Content-Type": "application/json"
114
+ },
115
+ body: JSON.stringify(payload)
116
+ });
117
+ if (!response.ok) {
118
+ const errorData = await response.json(), err = Error(`SendGrid API error: ${response.status} - ${JSON.stringify(errorData)}`);
119
+ err.status = response.status;
120
+ throw err;
121
+ }
122
+ log.info(`[${this.name}] Email sent successfully`, { attempt });
123
+ return response;
124
+ } catch (error) {
125
+ const status = error?.status;
126
+ if (!(typeof status === "number" && status >= 400 && status < 500 && status !== 429) && attempt < (config.services.sendgrid?.maxRetries ?? 3)) {
127
+ const retryTimeout = config.services.sendgrid?.retryTimeout ?? 1000;
128
+ log.warn(`[${this.name}] Email send failed, retrying (${attempt}/${config.services.sendgrid?.maxRetries ?? 3})`);
129
+ await new Promise((resolve) => setTimeout(resolve, retryTimeout));
130
+ return this.sendWithRetry(payload, attempt + 1);
131
+ }
132
+ throw error;
133
+ }
134
+ }
135
+ }
136
+ export default SendGridDriver;
@@ -0,0 +1,8 @@
1
+ import { BaseEmailDriver } from './base';
2
+ import type { EmailMessage, EmailResult } from '@stacksjs/types';
3
+ import type { TemplateOptions } from '../template';
4
+ export declare class SESDriver extends BaseEmailDriver {
5
+ name: string;
6
+ send(message: EmailMessage, options?: TemplateOptions): Promise<EmailResult>;
7
+ }
8
+ export default SESDriver;
@@ -0,0 +1,113 @@
1
+ import { config } from "@stacksjs/config";
2
+ import { SESClient } from "@stacksjs/ts-cloud";
3
+ import { template } from "../template";
4
+ import { buildMimeMessage } from "../mime";
5
+ import { filterStringHeaders } from "../validation";
6
+ import { BaseEmailDriver } from "./base";
7
+
8
+ export class SESDriver extends BaseEmailDriver {
9
+ name = "ses";
10
+ client = null;
11
+ getClient() {
12
+ if (!this.client) {
13
+ const sesConfig = config?.services?.ses, explicit = sesConfig?.credentials, hasExplicit = !!(explicit?.accessKeyId && explicit?.secretAccessKey);
14
+ this.client = new SESClient(sesConfig?.region || "us-east-1", hasExplicit ? {
15
+ accessKeyId: explicit.accessKeyId,
16
+ secretAccessKey: explicit.secretAccessKey,
17
+ sessionToken: explicit.sessionToken
18
+ } : void 0);
19
+ }
20
+ return this.client;
21
+ }
22
+ async send(message, options) {
23
+ try {
24
+ this.validateMessage(message);
25
+ let htmlContent;
26
+ if (message.template) {
27
+ const templ = await template(message.template, options);
28
+ if (templ && "html" in templ)
29
+ htmlContent = templ.html;
30
+ }
31
+ const finalHtml = htmlContent || message.html;
32
+ if (!finalHtml && !message.text)
33
+ throw Error("Email must have either HTML or text content");
34
+ const fromAddress = this.formatSourceAddress({
35
+ address: message.from?.address || config.email.from?.address || "",
36
+ name: message.from?.name || config.email.from?.name
37
+ }), 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);
38
+ if (!!(message.attachments && message.attachments.length > 0) || !!customHeaders) {
39
+ const raw = buildMimeMessage({
40
+ from: fromAddress,
41
+ to: toAddresses.join(", "),
42
+ cc: ccAddresses.length > 0 ? ccAddresses.join(", ") : void 0,
43
+ replyTo: replyToAddresses.length > 0 ? replyToAddresses.join(", ") : void 0,
44
+ customHeaders,
45
+ subject: message.subject,
46
+ text: message.text,
47
+ html: finalHtml,
48
+ attachments: message.attachments,
49
+ messageIdDomain: config.email.domain
50
+ }), result = await this.getClient().sendRawEmail({
51
+ source: fromAddress,
52
+ destinations: [...toAddresses, ...ccAddresses, ...bccAddresses],
53
+ rawMessage: raw
54
+ });
55
+ return this.handleSuccess(message, result.MessageId);
56
+ }
57
+ const body = {};
58
+ if (finalHtml)
59
+ body.Html = { Charset: config.email.charset || "UTF-8", Data: finalHtml };
60
+ if (message.text)
61
+ body.Text = { Charset: config.email.charset || "UTF-8", Data: message.text };
62
+ const result = await this.getClient().sendEmail({
63
+ FromEmailAddress: fromAddress,
64
+ Destination: {
65
+ ToAddresses: toAddresses,
66
+ CcAddresses: ccAddresses,
67
+ BccAddresses: bccAddresses
68
+ },
69
+ ...replyToAddresses.length > 0 ? { ReplyToAddresses: replyToAddresses } : {},
70
+ Content: {
71
+ Simple: {
72
+ Subject: {
73
+ Charset: config.email.charset || "UTF-8",
74
+ Data: message.subject
75
+ },
76
+ Body: body
77
+ }
78
+ }
79
+ });
80
+ return this.handleSuccess(message, result.MessageId);
81
+ } catch (error) {
82
+ return this.handleError(this.enrichSesError(error), message);
83
+ }
84
+ }
85
+ formatSourceAddress(from) {
86
+ if (!from.name)
87
+ return from.address;
88
+ return `${/[",()<>[\]:;@\\]/.test(from.name) ? `"${from.name.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"` : from.name} <${from.address}>`;
89
+ }
90
+ enrichSesError(error) {
91
+ const err = error instanceof Error ? error : Error(String(error)), text = `${err.message} ${err.name ?? ""}`.toLowerCase(), region = config?.services?.ses?.region || "us-east-1";
92
+ if (text.includes("email address is not verified") || text.includes("not authorized to send") || text.includes("messagerejected") && text.includes("verified")) {
93
+ err.message = `${err.message}
94
+
95
+ 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.`;
96
+ return err;
97
+ }
98
+ if (text.includes("signaturedoesnotmatch") || text.includes("invalidclienttokenid") || text.includes("unable to locate credentials") || text.includes("the security token included in the request is invalid")) {
99
+ err.message = `${err.message}
100
+
101
+ SES authentication failed. Check AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (or services.ses.credentials), and confirm the IAM principal has \`ses:SendEmail\` permission for the From identity.`;
102
+ return err;
103
+ }
104
+ if (text.includes("could not be reached") || text.includes("econnrefused") || text.includes("enotfound")) {
105
+ err.message = `${err.message}
106
+
107
+ SES endpoint unreachable. The configured region (\`${region}\`) must match the region where the From identity is verified.`;
108
+ return err;
109
+ }
110
+ return err;
111
+ }
112
+ }
113
+ export default SESDriver;
@@ -0,0 +1,14 @@
1
+ import { BaseEmailDriver } from './base';
2
+ import type { EmailAddress, EmailMessage, EmailResult } from '@stacksjs/types';
3
+ import type { TemplateOptions } from '../template';
4
+ /**
5
+ * SMTP Driver for email sending
6
+ * Works with any SMTP server: Mailtrap, Mailgun, SendGrid, SES, etc.
7
+ * Supports STARTTLS (port 587) and direct TLS (port 465)
8
+ */
9
+ export declare class SMTPDriver extends BaseEmailDriver {
10
+ name: string;
11
+ send(message: EmailMessage, options?: TemplateOptions): Promise<EmailResult>;
12
+ protected formatAddresses(addresses: string | string[] | EmailAddress[] | undefined): string[];
13
+ }
14
+ export default SMTPDriver;