@stacksjs/email 0.69.3 → 0.70.1

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.
@@ -0,0 +1,141 @@
1
+ import type { EmailAddress, EmailMessage, EmailResult, RenderOptions } from '@stacksjs/types';
2
+
3
+ export declare class MailgunDriver extends BaseEmailDriver {
4
+ public name = 'mailgun'
5
+ private apiKey: string
6
+ private domain: string
7
+ private endpoint: string
8
+
9
+ constructor() {
10
+ super()
11
+ this.apiKey = config.services.mailgun?.apiKey ?? ''
12
+ this.domain = config.services.mailgun?.domain ?? ''
13
+ this.endpoint = config.services.mailgun?.endpoint ?? 'api.mailgun.net'
14
+ }
15
+
16
+ public async send(message: EmailMessage, options?: RenderOptions): Promise<EmailResult> {
17
+ const logContext = {
18
+ provider: this.name,
19
+ to: message.to,
20
+ subject: message.subject,
21
+ domain: this.domain,
22
+ }
23
+
24
+ log.info('Sending email via Mailgun...', logContext)
25
+
26
+ try {
27
+ this.validateMessage(message)
28
+
29
+ let htmlContent: string | undefined
30
+ if (message.template) {
31
+ const templ = await template(message.template, options)
32
+ if (templ && 'html' in templ) {
33
+ htmlContent = templ.html
34
+ }
35
+ }
36
+
37
+ const formData = new FormData()
38
+ formData.append('from', this.formatMailgunAddress(message.from))
39
+
40
+ this.formatMailgunAddresses(message.to).forEach(to => formData.append('to', to))
41
+
42
+ if (message.cc)
43
+ this.formatMailgunAddresses(message.cc).forEach(cc => formData.append('cc', cc))
44
+
45
+ if (message.bcc)
46
+ this.formatMailgunAddresses(message.bcc).forEach(bcc => formData.append('bcc', bcc))
47
+
48
+ formData.append('subject', message.subject)
49
+
50
+ if (htmlContent) {
51
+ formData.append('html', htmlContent)
52
+ }
53
+
54
+ if (message.text)
55
+ formData.append('text', message.text)
56
+
57
+ if (message.attachments) {
58
+ message.attachments.forEach((attachment) => {
59
+ const content = typeof attachment.content === 'string'
60
+ ? attachment.content
61
+ : this.arrayBufferToBase64(attachment.content)
62
+
63
+ formData.append('attachment', new Blob([content], { type: attachment.contentType }), attachment.filename)
64
+ })
65
+ }
66
+
67
+ const response = await this.sendWithRetry(formData)
68
+ return this.handleSuccess(message, response.id)
69
+ }
70
+ catch (error) {
71
+ return this.handleError(error, message)
72
+ }
73
+ }
74
+
75
+ private formatMailgunAddress(address: EmailAddress): string {
76
+ return address.name ? `${address.name} <${address.address}>` : address.address
77
+ }
78
+
79
+ private formatMailgunAddresses(addresses: string | string[] | EmailAddress[] | undefined): string[] {
80
+ if (!addresses)
81
+ return []
82
+
83
+ if (typeof addresses === 'string')
84
+ return [addresses]
85
+
86
+ return addresses.map((addr) => {
87
+ if (typeof addr === 'string')
88
+ return addr
89
+ return addr.name ? `${addr.name} <${addr.address}>` : addr.address
90
+ })
91
+ }
92
+
93
+ private arrayBufferToBase64(buffer: Uint8Array): string {
94
+ let binary = ''
95
+ const bytes = new Uint8Array(buffer)
96
+ const len = bytes.byteLength
97
+
98
+ for (let i = 0; i < len; i++) {
99
+ binary += String.fromCharCode(bytes[i])
100
+ }
101
+
102
+ return typeof btoa === 'function'
103
+ ? btoa(binary)
104
+ : Buffer.from(binary).toString('base64')
105
+ }
106
+
107
+ private async sendWithRetry(formData: FormData, attempt = 1): Promise<any> {
108
+ const url = `https:
109
+ const auth = Buffer.from(`api:${this.apiKey}`).toString('base64')
110
+
111
+ try {
112
+ const response = await fetch(url, {
113
+ method: 'POST',
114
+ headers: {
115
+ Authorization: `Basic ${auth}`,
116
+ },
117
+ body: formData,
118
+ })
119
+
120
+ if (!response.ok) {
121
+ const errorData = await response.json()
122
+ throw new Error(`Mailgun API error: ${response.status} - ${JSON.stringify(errorData)}`)
123
+ }
124
+
125
+ const data = await response.json()
126
+ log.info(`[${this.name}] Email sent successfully`, { attempt, messageId: data.id })
127
+ return data
128
+ }
129
+ catch (error) {
130
+ if (attempt < (config.services.mailgun?.maxRetries ?? 3)) {
131
+ const retryTimeout = config.services.mailgun?.retryTimeout ?? 1000
132
+ log.warn(`[${this.name}] Email send failed, retrying (${attempt}/${config.services.mailgun?.maxRetries ?? 3})`)
133
+ await new Promise(resolve => setTimeout(resolve, retryTimeout))
134
+ return this.sendWithRetry(formData, attempt + 1)
135
+ }
136
+ throw error
137
+ }
138
+ }
139
+ }
140
+
141
+ export default MailgunDriver;
@@ -0,0 +1,131 @@
1
+ import type { EmailMessage, EmailResult, MailtrapResponse, RenderOptions } from '@stacksjs/types';
2
+
3
+ export declare class MailtrapDriver extends BaseEmailDriver {
4
+ public name = 'mailtrap'
5
+ private host: string
6
+ private token: string
7
+ private inboxId?: number
8
+
9
+ constructor() {
10
+ super()
11
+ this.host = config.services.mailtrap?.host ?? 'https:
12
+ this.token = config.services.mailtrap?.token ?? ''
13
+ this.inboxId = config.services.mailtrap?.inboxId ? Number(config.services.mailtrap.inboxId) : undefined
14
+ }
15
+
16
+ public async send(message: EmailMessage, options?: RenderOptions): Promise<EmailResult> {
17
+ const logContext = {
18
+ provider: this.name,
19
+ to: message.to,
20
+ subject: message.subject,
21
+ inboxId: this.inboxId,
22
+ }
23
+
24
+ log.info('Sending email via Mailtrap...', logContext)
25
+
26
+ try {
27
+ this.validateMessage(message)
28
+ let templ
29
+ if (message.template)
30
+ templ = await template(message.template, options)
31
+
32
+ const mailtrapPayload = {
33
+ from: {
34
+ email: message.from.address || config.email.from?.address,
35
+ ...(message.from.name && { name: message.from.name }),
36
+ },
37
+ to: this.formatMailtrapAddresses(message.to),
38
+ ...(message.cc && { cc: this.formatMailtrapAddresses(message.cc) }),
39
+ ...(message.bcc && { bcc: this.formatMailtrapAddresses(message.bcc) }),
40
+ subject: message.subject,
41
+ ...(templ?.html && { html: templ.html }),
42
+ ...(message.text && { text: message.text }),
43
+ ...(message.attachments && {
44
+ attachments: message.attachments.map(attachment => ({
45
+ filename: attachment.filename,
46
+ content: typeof attachment.content === 'string'
47
+ ? attachment.content
48
+ : this.arrayBufferToBase64(attachment.content),
49
+ type: attachment.contentType || 'application/octet-stream',
50
+ })),
51
+ }),
52
+ }
53
+
54
+ const response = await this.sendWithRetry(mailtrapPayload)
55
+ return this.handleSuccess(message, response.message_ids?.[0])
56
+ }
57
+ catch (error) {
58
+ return this.handleError(error, message)
59
+ }
60
+ }
61
+
62
+ private formatMailtrapAddresses(addresses: string | string[] | EmailAddress[] | undefined): Array<{ email: string, name?: string }> {
63
+ if (!addresses)
64
+ return []
65
+
66
+ if (typeof addresses === 'string') {
67
+ return [{ email: addresses }]
68
+ }
69
+
70
+ return addresses.map((addr) => {
71
+ if (typeof addr === 'string')
72
+ return { email: addr }
73
+ return { email: addr.address, ...(addr.name && { name: addr.name }) }
74
+ })
75
+ }
76
+
77
+ private arrayBufferToBase64(buffer: Uint8Array): string {
78
+ let binary = ''
79
+ const bytes = new Uint8Array(buffer)
80
+ const len = bytes.byteLength
81
+
82
+ for (let i = 0; i < len; i++) {
83
+ binary += String.fromCharCode(bytes[i])
84
+ }
85
+
86
+ return typeof btoa === 'function'
87
+ ? btoa(binary)
88
+ : Buffer.from(binary).toString('base64')
89
+ }
90
+
91
+ private async sendWithRetry(payload: any, attempt = 1): Promise<any> {
92
+ if (!this.inboxId) {
93
+ throw new Error('Mailtrap inbox ID is required but not provided. Please set MAILTRAP_INBOX_ID in your environment variables.')
94
+ }
95
+
96
+ const endpoint = `${this.host}/${this.inboxId}`
97
+
98
+ try {
99
+ const response = await fetch(endpoint, {
100
+ method: 'POST',
101
+ headers: {
102
+ 'Authorization': `Bearer ${this.token}`,
103
+ 'Content-Type': 'application/json',
104
+ },
105
+ body: JSON.stringify(payload),
106
+ })
107
+
108
+ if (!response.ok) {
109
+ const errorData = await response.json()
110
+ throw new Error(`Mailtrap API error: ${response.status} - ${JSON.stringify(errorData)}`)
111
+ }
112
+
113
+ const data: MailtrapResponse = await (response.json() as Promise<MailtrapResponse>)
114
+
115
+ log.info(`[${this.name}] Email sent successfully`, { attempt, messageId: data.message_ids?.[0] })
116
+ return data
117
+ }
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
+
124
+ return this.sendWithRetry(payload, attempt + 1)
125
+ }
126
+ throw error
127
+ }
128
+ }
129
+ }
130
+
131
+ export default MailtrapDriver;
@@ -0,0 +1,146 @@
1
+ import type { EmailMessage, EmailResult, RenderOptions } from '@stacksjs/types';
2
+
3
+ export declare class SendGridDriver extends BaseEmailDriver {
4
+ public name = 'sendgrid'
5
+ private apiKey: string
6
+
7
+ constructor() {
8
+ super()
9
+ this.apiKey = config.services.sendgrid?.apiKey ?? ''
10
+ }
11
+
12
+ public async send(message: EmailMessage, options?: RenderOptions): Promise<EmailResult> {
13
+ const logContext = {
14
+ provider: this.name,
15
+ to: message.to,
16
+ subject: message.subject,
17
+ }
18
+
19
+ log.info('Sending email via SendGrid...', logContext)
20
+
21
+ try {
22
+ this.validateMessage(message)
23
+
24
+ let htmlContent: string | undefined
25
+ if (message.template) {
26
+ const templ = await template(message.template, options)
27
+ if (templ && 'html' in templ) {
28
+ htmlContent = templ.html
29
+ }
30
+ }
31
+
32
+ const content = []
33
+
34
+ if (htmlContent) {
35
+ content.push({
36
+ type: 'text/html',
37
+ value: htmlContent,
38
+ })
39
+ }
40
+
41
+ if (message.text) {
42
+ content.push({
43
+ type: 'text/plain',
44
+ value: message.text,
45
+ })
46
+ }
47
+
48
+ if (content.length === 0) {
49
+ throw new Error('Email must have either HTML or text content')
50
+ }
51
+
52
+ const sendgridPayload = {
53
+ personalizations: [
54
+ {
55
+ to: this.formatSendGridAddresses(message.to),
56
+ ...(message.cc && { cc: this.formatSendGridAddresses(message.cc) }),
57
+ ...(message.bcc && { bcc: this.formatSendGridAddresses(message.bcc) }),
58
+ subject: message.subject,
59
+ },
60
+ ],
61
+ from: {
62
+ email: message.from.address || config.email.from?.address,
63
+ ...(message.from.name && { name: message.from.name }),
64
+ },
65
+ content,
66
+ ...(message.attachments && {
67
+ attachments: message.attachments.map(attachment => ({
68
+ filename: attachment.filename,
69
+ content: typeof attachment.content === 'string'
70
+ ? attachment.content
71
+ : this.arrayBufferToBase64(attachment.content),
72
+ type: attachment.contentType,
73
+ disposition: 'attachment',
74
+ })),
75
+ }),
76
+ }
77
+
78
+ const response = await this.sendWithRetry(sendgridPayload)
79
+ return this.handleSuccess(message, response.headers?.['x-message-id'])
80
+ }
81
+ catch (error) {
82
+ return this.handleError(error, message)
83
+ }
84
+ }
85
+
86
+ private formatSendGridAddresses(addresses: string | string[] | EmailAddress[] | undefined): Array<{ email: string, name?: string }> {
87
+ if (!addresses)
88
+ return []
89
+
90
+ if (typeof addresses === 'string') {
91
+ return [{ email: addresses }]
92
+ }
93
+
94
+ return addresses.map((addr) => {
95
+ if (typeof addr === 'string')
96
+ return { email: addr }
97
+ return { email: addr.address, ...(addr.name && { name: addr.name }) }
98
+ })
99
+ }
100
+
101
+ private arrayBufferToBase64(buffer: Uint8Array): string {
102
+ let binary = ''
103
+ const bytes = new Uint8Array(buffer)
104
+ const len = bytes.byteLength
105
+
106
+ for (let i = 0; i < len; i++) {
107
+ binary += String.fromCharCode(bytes[i])
108
+ }
109
+
110
+ return typeof btoa === 'function'
111
+ ? btoa(binary)
112
+ : Buffer.from(binary).toString('base64')
113
+ }
114
+
115
+ private async sendWithRetry(payload: any, attempt = 1): Promise<any> {
116
+ try {
117
+ const response = await fetch('https:
118
+ method: 'POST',
119
+ headers: {
120
+ 'Authorization': `Bearer ${this.apiKey}`,
121
+ 'Content-Type': 'application/json',
122
+ },
123
+ body: JSON.stringify(payload),
124
+ })
125
+
126
+ if (!response.ok) {
127
+ const errorData = await response.json()
128
+ throw new Error(`SendGrid API error: ${response.status} - ${JSON.stringify(errorData)}`)
129
+ }
130
+
131
+ log.info(`[${this.name}] Email sent successfully`, { attempt })
132
+ return response
133
+ }
134
+ catch (error) {
135
+ if (attempt < (config.services.sendgrid?.maxRetries ?? 3)) {
136
+ const retryTimeout = config.services.sendgrid?.retryTimeout ?? 1000
137
+ log.warn(`[${this.name}] Email send failed, retrying (${attempt}/${config.services.sendgrid?.maxRetries ?? 3})`)
138
+ await new Promise(resolve => setTimeout(resolve, retryTimeout))
139
+ return this.sendWithRetry(payload, attempt + 1)
140
+ }
141
+ throw error
142
+ }
143
+ }
144
+ }
145
+
146
+ export default SendGridDriver;
package/dist/ses.d.ts ADDED
@@ -0,0 +1,80 @@
1
+ import type { EmailMessage, EmailResult, RenderOptions } from '@stacksjs/types';
2
+
3
+ export declare class SESDriver extends BaseEmailDriver {
4
+ public name = 'ses'
5
+ private client: SES
6
+
7
+ constructor() {
8
+ super()
9
+
10
+ const credentials = {
11
+ accessKeyId: config.services.ses?.credentials?.accessKeyId ?? '',
12
+ secretAccessKey: config.services.ses?.credentials?.secretAccessKey ?? '',
13
+ }
14
+
15
+ this.client = new SES({
16
+ region: config.services.ses?.region || 'us-east-1',
17
+ credentials,
18
+ })
19
+ }
20
+
21
+ public async send(message: EmailMessage, options?: RenderOptions): Promise<EmailResult> {
22
+ try {
23
+ this.validateMessage(message)
24
+
25
+ let htmlContent: string | undefined
26
+ if (message.template) {
27
+ const templ = await template(message.template, options)
28
+ if (templ && 'html' in templ) {
29
+ htmlContent = templ.html
30
+ }
31
+ }
32
+
33
+ const messageBody: any = {}
34
+
35
+ if (htmlContent) {
36
+ messageBody.Html = {
37
+ Charset: config.email.charset || 'UTF-8',
38
+ Data: htmlContent,
39
+ }
40
+ }
41
+
42
+ if (message.text) {
43
+ messageBody.Text = {
44
+ Charset: config.email.charset || 'UTF-8',
45
+ Data: message.text,
46
+ }
47
+ }
48
+
49
+ if (Object.keys(messageBody).length === 0) {
50
+ throw new Error('Email must have either HTML or text content')
51
+ }
52
+
53
+ const params = {
54
+ Source: message.from?.address || config.email.from?.address,
55
+
56
+ Destination: {
57
+ ToAddresses: this.formatAddresses(message.to),
58
+ CcAddresses: this.formatAddresses(message.cc),
59
+ BccAddresses: this.formatAddresses(message.bcc),
60
+ },
61
+
62
+ Message: {
63
+ Body: messageBody,
64
+ Subject: {
65
+ Charset: config.email.charset || 'UTF-8',
66
+ Data: message.subject,
67
+ },
68
+ },
69
+ }
70
+
71
+ const response = await this.client.send(new SendEmailCommand(params))
72
+ return this.handleSuccess(message, response.MessageId)
73
+ }
74
+ catch (error) {
75
+ return this.handleError(error, message)
76
+ }
77
+ }
78
+ }
79
+
80
+ export default SESDriver;
@@ -1,7 +1,7 @@
1
- import type { I18n } from 'vue-email';
1
+ import type { RenderOptions } from '@stacksjs/types';
2
2
 
3
- export declare interface RenderOptions {
4
- props?: Record<string, unknown>
5
- i18n?: I18n
3
+ declare interface HtmlResult {
4
+ html: string
5
+ text: string
6
6
  }
7
- export declare function template(path: string, options?: RenderOptions): Promise<string>;
7
+ export declare function template(path: string, options?: RenderOptions): Promise<HtmlResult>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/email",
3
3
  "type": "module",
4
- "version": "0.69.3",
4
+ "version": "0.70.1",
5
5
  "description": "The Stacks Email integration. Painlessly create & manage your inboxes, templates, and send emails.",
6
6
  "author": "Chris Breuer",
7
7
  "contributors": ["Chris Breuer <chris@stacksjs.org>"],
@@ -35,11 +35,11 @@
35
35
  "prepublishOnly": "bun run build"
36
36
  },
37
37
  "devDependencies": {
38
- "@stacksjs/cli": "0.69.2",
39
- "@stacksjs/config": "0.69.2",
40
- "@stacksjs/development": "0.69.2",
41
- "@stacksjs/error-handling": "0.69.2",
42
- "@stacksjs/types": "0.69.2",
38
+ "@stacksjs/cli": "0.70.1",
39
+ "@stacksjs/config": "0.70.1",
40
+ "@stacksjs/development": "0.70.1",
41
+ "@stacksjs/error-handling": "0.70.1",
42
+ "@stacksjs/types": "0.70.1",
43
43
  "aws-sdk": "^2.1692.0",
44
44
  "vue-email": "^0.8.11"
45
45
  }