@veloxts/mail 0.6.51
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/GUIDE.md +132 -0
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +70 -0
- package/dist/mail.d.ts +47 -0
- package/dist/mail.js +55 -0
- package/dist/manager.d.ts +82 -0
- package/dist/manager.js +153 -0
- package/dist/plugin.d.ts +117 -0
- package/dist/plugin.js +145 -0
- package/dist/transports/log.d.ts +32 -0
- package/dist/transports/log.js +101 -0
- package/dist/transports/resend.d.ts +31 -0
- package/dist/transports/resend.js +95 -0
- package/dist/transports/smtp.d.ts +36 -0
- package/dist/transports/smtp.js +116 -0
- package/dist/types.d.ts +425 -0
- package/dist/types.js +6 -0
- package/dist/utils.d.ts +89 -0
- package/dist/utils.js +190 -0
- package/package.json +73 -0
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mail Plugin
|
|
3
|
+
*
|
|
4
|
+
* VeloxTS plugin for integrating email functionality into the framework.
|
|
5
|
+
*/
|
|
6
|
+
import type { FastifyInstance } from 'fastify';
|
|
7
|
+
import '@veloxts/core';
|
|
8
|
+
import { type MailManager } from './manager.js';
|
|
9
|
+
import type { MailPluginOptions } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Symbol for storing mail manager on Fastify instance.
|
|
12
|
+
* Using a symbol prevents naming conflicts with other plugins.
|
|
13
|
+
*/
|
|
14
|
+
declare const MAIL_MANAGER_KEY: unique symbol;
|
|
15
|
+
/**
|
|
16
|
+
* Extend Fastify types with mail manager.
|
|
17
|
+
*/
|
|
18
|
+
declare module 'fastify' {
|
|
19
|
+
interface FastifyInstance {
|
|
20
|
+
[MAIL_MANAGER_KEY]?: MailManager;
|
|
21
|
+
}
|
|
22
|
+
interface FastifyRequest {
|
|
23
|
+
mail?: MailManager;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Extend VeloxTS BaseContext with mail manager.
|
|
28
|
+
*
|
|
29
|
+
* This enables `ctx.mail` in procedure handlers with full type safety
|
|
30
|
+
* and autocomplete when the mail plugin is registered.
|
|
31
|
+
*
|
|
32
|
+
* The property is NON-optional since the plugin guarantees it exists
|
|
33
|
+
* after registration.
|
|
34
|
+
*/
|
|
35
|
+
declare module '@veloxts/core' {
|
|
36
|
+
interface BaseContext {
|
|
37
|
+
/** Mail manager for sending emails via templates */
|
|
38
|
+
mail: MailManager;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Create the mail plugin for VeloxTS.
|
|
43
|
+
*
|
|
44
|
+
* Each Fastify instance gets its own mail manager, ensuring proper test isolation
|
|
45
|
+
* and supporting multiple Fastify instances in the same process.
|
|
46
|
+
*
|
|
47
|
+
* @param options - Mail plugin options
|
|
48
|
+
* @returns Fastify plugin
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* import { createApp } from '@veloxts/core';
|
|
53
|
+
* import { mailPlugin } from '@veloxts/mail';
|
|
54
|
+
*
|
|
55
|
+
* const app = createApp();
|
|
56
|
+
*
|
|
57
|
+
* app.use(mailPlugin({
|
|
58
|
+
* driver: 'resend',
|
|
59
|
+
* config: { apiKey: process.env.RESEND_API_KEY! },
|
|
60
|
+
* from: { email: 'hello@myapp.com', name: 'My App' },
|
|
61
|
+
* }));
|
|
62
|
+
*
|
|
63
|
+
* // In procedures:
|
|
64
|
+
* await ctx.mail.send(WelcomeEmail, {
|
|
65
|
+
* to: 'user@example.com',
|
|
66
|
+
* data: { user, activationUrl },
|
|
67
|
+
* });
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export declare function mailPlugin(options?: MailPluginOptions): (fastify: FastifyInstance) => Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Get the mail manager from a Fastify instance.
|
|
73
|
+
*
|
|
74
|
+
* @param fastify - Fastify instance with mail plugin registered
|
|
75
|
+
* @throws Error if mail plugin is not registered
|
|
76
|
+
*/
|
|
77
|
+
export declare function getMailFromInstance(fastify: FastifyInstance): MailManager;
|
|
78
|
+
/**
|
|
79
|
+
* Initialize mail manager standalone (without Fastify).
|
|
80
|
+
*
|
|
81
|
+
* Useful for CLI commands or background jobs. This creates a separate
|
|
82
|
+
* mail instance that is independent from any Fastify instances.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* import { initMail, closeMail } from '@veloxts/mail';
|
|
87
|
+
*
|
|
88
|
+
* const mail = await initMail({
|
|
89
|
+
* driver: 'resend',
|
|
90
|
+
* config: { apiKey: process.env.RESEND_API_KEY! },
|
|
91
|
+
* from: { email: 'hello@myapp.com', name: 'My App' },
|
|
92
|
+
* });
|
|
93
|
+
*
|
|
94
|
+
* // Use mail directly
|
|
95
|
+
* await mail.send(WelcomeEmail, { to: 'user@example.com', data: { ... } });
|
|
96
|
+
*
|
|
97
|
+
* // Clean up when done
|
|
98
|
+
* await closeMail();
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export declare function initMail(options?: MailPluginOptions): Promise<MailManager>;
|
|
102
|
+
/**
|
|
103
|
+
* Get the standalone mail manager.
|
|
104
|
+
*
|
|
105
|
+
* @throws Error if mail is not initialized via initMail()
|
|
106
|
+
*/
|
|
107
|
+
export declare function getMail(): MailManager;
|
|
108
|
+
/**
|
|
109
|
+
* Close the standalone mail connection.
|
|
110
|
+
*/
|
|
111
|
+
export declare function closeMail(): Promise<void>;
|
|
112
|
+
/**
|
|
113
|
+
* Reset standalone mail instance (for testing purposes).
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
export declare function _resetStandaloneMail(): void;
|
|
117
|
+
export {};
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mail Plugin
|
|
3
|
+
*
|
|
4
|
+
* VeloxTS plugin for integrating email functionality into the framework.
|
|
5
|
+
*/
|
|
6
|
+
import fp from 'fastify-plugin';
|
|
7
|
+
// Side-effect import to enable module augmentation (TypeScript requires module reference)
|
|
8
|
+
import '@veloxts/core';
|
|
9
|
+
import { createMailManager } from './manager.js';
|
|
10
|
+
/**
|
|
11
|
+
* Symbol for storing mail manager on Fastify instance.
|
|
12
|
+
* Using a symbol prevents naming conflicts with other plugins.
|
|
13
|
+
*/
|
|
14
|
+
const MAIL_MANAGER_KEY = Symbol.for('@veloxts/mail:manager');
|
|
15
|
+
/**
|
|
16
|
+
* Standalone mail instance for CLI commands and background jobs.
|
|
17
|
+
* This is separate from the plugin to avoid test isolation issues.
|
|
18
|
+
*/
|
|
19
|
+
let standaloneMailInstance = null;
|
|
20
|
+
/**
|
|
21
|
+
* Create the mail plugin for VeloxTS.
|
|
22
|
+
*
|
|
23
|
+
* Each Fastify instance gets its own mail manager, ensuring proper test isolation
|
|
24
|
+
* and supporting multiple Fastify instances in the same process.
|
|
25
|
+
*
|
|
26
|
+
* @param options - Mail plugin options
|
|
27
|
+
* @returns Fastify plugin
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* import { createApp } from '@veloxts/core';
|
|
32
|
+
* import { mailPlugin } from '@veloxts/mail';
|
|
33
|
+
*
|
|
34
|
+
* const app = createApp();
|
|
35
|
+
*
|
|
36
|
+
* app.use(mailPlugin({
|
|
37
|
+
* driver: 'resend',
|
|
38
|
+
* config: { apiKey: process.env.RESEND_API_KEY! },
|
|
39
|
+
* from: { email: 'hello@myapp.com', name: 'My App' },
|
|
40
|
+
* }));
|
|
41
|
+
*
|
|
42
|
+
* // In procedures:
|
|
43
|
+
* await ctx.mail.send(WelcomeEmail, {
|
|
44
|
+
* to: 'user@example.com',
|
|
45
|
+
* data: { user, activationUrl },
|
|
46
|
+
* });
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function mailPlugin(options = {}) {
|
|
50
|
+
return fp(async (fastify) => {
|
|
51
|
+
// Create a new mail manager for this Fastify instance
|
|
52
|
+
const mailManager = await createMailManager(options);
|
|
53
|
+
// Store on Fastify instance using symbol key (type-safe via Object.defineProperty)
|
|
54
|
+
Object.defineProperty(fastify, MAIL_MANAGER_KEY, {
|
|
55
|
+
value: mailManager,
|
|
56
|
+
writable: false,
|
|
57
|
+
enumerable: false,
|
|
58
|
+
configurable: false,
|
|
59
|
+
});
|
|
60
|
+
// Decorate the request with mail manager
|
|
61
|
+
fastify.decorateRequest('mail', undefined);
|
|
62
|
+
// Add mail to request context
|
|
63
|
+
fastify.addHook('onRequest', async (request) => {
|
|
64
|
+
request.mail = mailManager;
|
|
65
|
+
});
|
|
66
|
+
// Close mail on server shutdown
|
|
67
|
+
fastify.addHook('onClose', async () => {
|
|
68
|
+
await mailManager.close();
|
|
69
|
+
});
|
|
70
|
+
}, {
|
|
71
|
+
name: '@veloxts/mail',
|
|
72
|
+
fastify: '5.x',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get the mail manager from a Fastify instance.
|
|
77
|
+
*
|
|
78
|
+
* @param fastify - Fastify instance with mail plugin registered
|
|
79
|
+
* @throws Error if mail plugin is not registered
|
|
80
|
+
*/
|
|
81
|
+
export function getMailFromInstance(fastify) {
|
|
82
|
+
// Type-safe property access using Object.getOwnPropertyDescriptor
|
|
83
|
+
const descriptor = Object.getOwnPropertyDescriptor(fastify, MAIL_MANAGER_KEY);
|
|
84
|
+
const mail = descriptor?.value;
|
|
85
|
+
if (!mail) {
|
|
86
|
+
throw new Error('Mail not initialized on this Fastify instance. Make sure to register mailPlugin first.');
|
|
87
|
+
}
|
|
88
|
+
return mail;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Initialize mail manager standalone (without Fastify).
|
|
92
|
+
*
|
|
93
|
+
* Useful for CLI commands or background jobs. This creates a separate
|
|
94
|
+
* mail instance that is independent from any Fastify instances.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* import { initMail, closeMail } from '@veloxts/mail';
|
|
99
|
+
*
|
|
100
|
+
* const mail = await initMail({
|
|
101
|
+
* driver: 'resend',
|
|
102
|
+
* config: { apiKey: process.env.RESEND_API_KEY! },
|
|
103
|
+
* from: { email: 'hello@myapp.com', name: 'My App' },
|
|
104
|
+
* });
|
|
105
|
+
*
|
|
106
|
+
* // Use mail directly
|
|
107
|
+
* await mail.send(WelcomeEmail, { to: 'user@example.com', data: { ... } });
|
|
108
|
+
*
|
|
109
|
+
* // Clean up when done
|
|
110
|
+
* await closeMail();
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export async function initMail(options = {}) {
|
|
114
|
+
if (!standaloneMailInstance) {
|
|
115
|
+
standaloneMailInstance = await createMailManager(options);
|
|
116
|
+
}
|
|
117
|
+
return standaloneMailInstance;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get the standalone mail manager.
|
|
121
|
+
*
|
|
122
|
+
* @throws Error if mail is not initialized via initMail()
|
|
123
|
+
*/
|
|
124
|
+
export function getMail() {
|
|
125
|
+
if (!standaloneMailInstance) {
|
|
126
|
+
throw new Error('Standalone mail not initialized. Call initMail() first, or use getMailFromInstance() for Fastify-based usage.');
|
|
127
|
+
}
|
|
128
|
+
return standaloneMailInstance;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Close the standalone mail connection.
|
|
132
|
+
*/
|
|
133
|
+
export async function closeMail() {
|
|
134
|
+
if (standaloneMailInstance) {
|
|
135
|
+
await standaloneMailInstance.close();
|
|
136
|
+
standaloneMailInstance = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Reset standalone mail instance (for testing purposes).
|
|
141
|
+
* @internal
|
|
142
|
+
*/
|
|
143
|
+
export function _resetStandaloneMail() {
|
|
144
|
+
standaloneMailInstance = null;
|
|
145
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Transport Driver
|
|
3
|
+
*
|
|
4
|
+
* Development transport that logs emails to console instead of sending.
|
|
5
|
+
*/
|
|
6
|
+
import type { LogConfig, MailTransport } from '../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Create a log mail transport for development.
|
|
9
|
+
*
|
|
10
|
+
* @param config - Log configuration
|
|
11
|
+
* @returns Mail transport implementation
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const transport = createLogTransport({
|
|
16
|
+
* showHtml: true, // Show full HTML in logs
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* await transport.send({
|
|
20
|
+
* from: { email: 'from@example.com', name: 'App' },
|
|
21
|
+
* to: [{ email: 'user@example.com' }],
|
|
22
|
+
* subject: 'Hello',
|
|
23
|
+
* html: '<h1>Hello World</h1>',
|
|
24
|
+
* });
|
|
25
|
+
* // Logs email details to console
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function createLogTransport(config?: LogConfig): MailTransport;
|
|
29
|
+
/**
|
|
30
|
+
* Log transport driver name.
|
|
31
|
+
*/
|
|
32
|
+
export declare const DRIVER_NAME: "log";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Transport Driver
|
|
3
|
+
*
|
|
4
|
+
* Development transport that logs emails to console instead of sending.
|
|
5
|
+
*/
|
|
6
|
+
import { formatAddress, generateMessageId } from '../utils.js';
|
|
7
|
+
/**
|
|
8
|
+
* Default log transport configuration.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_CONFIG = {
|
|
11
|
+
showHtml: false,
|
|
12
|
+
logger: console.log,
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Create a log mail transport for development.
|
|
16
|
+
*
|
|
17
|
+
* @param config - Log configuration
|
|
18
|
+
* @returns Mail transport implementation
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const transport = createLogTransport({
|
|
23
|
+
* showHtml: true, // Show full HTML in logs
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* await transport.send({
|
|
27
|
+
* from: { email: 'from@example.com', name: 'App' },
|
|
28
|
+
* to: [{ email: 'user@example.com' }],
|
|
29
|
+
* subject: 'Hello',
|
|
30
|
+
* html: '<h1>Hello World</h1>',
|
|
31
|
+
* });
|
|
32
|
+
* // Logs email details to console
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function createLogTransport(config = {}) {
|
|
36
|
+
const options = { ...DEFAULT_CONFIG, ...config };
|
|
37
|
+
const log = options.logger;
|
|
38
|
+
const transport = {
|
|
39
|
+
async send(sendOptions) {
|
|
40
|
+
const messageId = generateMessageId();
|
|
41
|
+
const separator = '─'.repeat(60);
|
|
42
|
+
log(`\n${separator}`);
|
|
43
|
+
log('📧 EMAIL SENT (log transport)');
|
|
44
|
+
log(separator);
|
|
45
|
+
log(`Message ID: ${messageId}`);
|
|
46
|
+
log(`From: ${formatAddress(sendOptions.from)}`);
|
|
47
|
+
log(`To: ${sendOptions.to.map(formatAddress).join(', ')}`);
|
|
48
|
+
if (sendOptions.cc?.length) {
|
|
49
|
+
log(`CC: ${sendOptions.cc.map(formatAddress).join(', ')}`);
|
|
50
|
+
}
|
|
51
|
+
if (sendOptions.bcc?.length) {
|
|
52
|
+
log(`BCC: ${sendOptions.bcc.map(formatAddress).join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
if (sendOptions.replyTo) {
|
|
55
|
+
log(`Reply-To: ${formatAddress(sendOptions.replyTo)}`);
|
|
56
|
+
}
|
|
57
|
+
log(`Subject: ${sendOptions.subject}`);
|
|
58
|
+
if (sendOptions.attachments?.length) {
|
|
59
|
+
log(`Attachments: ${sendOptions.attachments.map((a) => a.filename).join(', ')}`);
|
|
60
|
+
}
|
|
61
|
+
if (sendOptions.tags?.length) {
|
|
62
|
+
log(`Tags: ${sendOptions.tags.join(', ')}`);
|
|
63
|
+
}
|
|
64
|
+
if (sendOptions.headers && Object.keys(sendOptions.headers).length > 0) {
|
|
65
|
+
log('Headers:');
|
|
66
|
+
for (const [key, value] of Object.entries(sendOptions.headers)) {
|
|
67
|
+
log(` ${key}: ${value}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
log(separator);
|
|
71
|
+
if (sendOptions.text) {
|
|
72
|
+
log('Plain Text:');
|
|
73
|
+
log(sendOptions.text);
|
|
74
|
+
log(separator);
|
|
75
|
+
}
|
|
76
|
+
if (options.showHtml) {
|
|
77
|
+
log('HTML:');
|
|
78
|
+
log(sendOptions.html);
|
|
79
|
+
log(separator);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const htmlPreview = sendOptions.html.substring(0, 200);
|
|
83
|
+
log(`HTML Preview: ${htmlPreview}${sendOptions.html.length > 200 ? '...' : ''}`);
|
|
84
|
+
log(separator);
|
|
85
|
+
}
|
|
86
|
+
log('');
|
|
87
|
+
return {
|
|
88
|
+
success: true,
|
|
89
|
+
messageId,
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
async close() {
|
|
93
|
+
// Nothing to close
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
return transport;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Log transport driver name.
|
|
100
|
+
*/
|
|
101
|
+
export const DRIVER_NAME = 'log';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resend Transport Driver
|
|
3
|
+
*
|
|
4
|
+
* Email transport using Resend API.
|
|
5
|
+
*/
|
|
6
|
+
import type { MailTransport, ResendConfig } from '../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Create a Resend mail transport.
|
|
9
|
+
*
|
|
10
|
+
* @param config - Resend configuration
|
|
11
|
+
* @returns Mail transport implementation
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const transport = await createResendTransport({
|
|
16
|
+
* apiKey: process.env.RESEND_API_KEY!,
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* await transport.send({
|
|
20
|
+
* from: { email: 'from@example.com', name: 'App' },
|
|
21
|
+
* to: [{ email: 'user@example.com' }],
|
|
22
|
+
* subject: 'Hello',
|
|
23
|
+
* html: '<h1>Hello World</h1>',
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function createResendTransport(config: ResendConfig): Promise<MailTransport>;
|
|
28
|
+
/**
|
|
29
|
+
* Resend transport driver name.
|
|
30
|
+
*/
|
|
31
|
+
export declare const DRIVER_NAME: "resend";
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resend Transport Driver
|
|
3
|
+
*
|
|
4
|
+
* Email transport using Resend API.
|
|
5
|
+
*/
|
|
6
|
+
import { formatAddress } from '../utils.js';
|
|
7
|
+
/**
|
|
8
|
+
* Create a Resend mail transport.
|
|
9
|
+
*
|
|
10
|
+
* @param config - Resend configuration
|
|
11
|
+
* @returns Mail transport implementation
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const transport = await createResendTransport({
|
|
16
|
+
* apiKey: process.env.RESEND_API_KEY!,
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* await transport.send({
|
|
20
|
+
* from: { email: 'from@example.com', name: 'App' },
|
|
21
|
+
* to: [{ email: 'user@example.com' }],
|
|
22
|
+
* subject: 'Hello',
|
|
23
|
+
* html: '<h1>Hello World</h1>',
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export async function createResendTransport(config) {
|
|
28
|
+
// Dynamic import of resend
|
|
29
|
+
const { Resend } = await import('resend');
|
|
30
|
+
const resend = new Resend(config.apiKey);
|
|
31
|
+
/**
|
|
32
|
+
* Format addresses for Resend API.
|
|
33
|
+
*/
|
|
34
|
+
function formatAddresses(addresses) {
|
|
35
|
+
return addresses.map(formatAddress);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Convert attachment content to base64 for Resend API.
|
|
39
|
+
* Buffer is converted directly, string content is assumed to be raw and encoded.
|
|
40
|
+
*/
|
|
41
|
+
function encodeAttachmentContent(content) {
|
|
42
|
+
if (Buffer.isBuffer(content)) {
|
|
43
|
+
return content.toString('base64');
|
|
44
|
+
}
|
|
45
|
+
// String content is assumed to be raw - encode to base64
|
|
46
|
+
return Buffer.from(content, 'utf-8').toString('base64');
|
|
47
|
+
}
|
|
48
|
+
const transport = {
|
|
49
|
+
async send(sendOptions) {
|
|
50
|
+
try {
|
|
51
|
+
const result = await resend.emails.send({
|
|
52
|
+
from: formatAddress(sendOptions.from),
|
|
53
|
+
to: formatAddresses(sendOptions.to),
|
|
54
|
+
cc: sendOptions.cc ? formatAddresses(sendOptions.cc) : undefined,
|
|
55
|
+
bcc: sendOptions.bcc ? formatAddresses(sendOptions.bcc) : undefined,
|
|
56
|
+
replyTo: sendOptions.replyTo ? formatAddress(sendOptions.replyTo) : undefined,
|
|
57
|
+
subject: sendOptions.subject,
|
|
58
|
+
html: sendOptions.html,
|
|
59
|
+
text: sendOptions.text,
|
|
60
|
+
attachments: sendOptions.attachments?.map((att) => ({
|
|
61
|
+
filename: att.filename,
|
|
62
|
+
content: encodeAttachmentContent(att.content),
|
|
63
|
+
})),
|
|
64
|
+
headers: sendOptions.headers,
|
|
65
|
+
tags: sendOptions.tags?.map((tag) => ({ name: tag, value: 'true' })),
|
|
66
|
+
});
|
|
67
|
+
if (result.error) {
|
|
68
|
+
return {
|
|
69
|
+
success: false,
|
|
70
|
+
error: result.error.message,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
success: true,
|
|
75
|
+
messageId: result.data?.id,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
error: errorMessage,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
async close() {
|
|
87
|
+
// Resend client doesn't need explicit closing
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
return transport;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Resend transport driver name.
|
|
94
|
+
*/
|
|
95
|
+
export const DRIVER_NAME = 'resend';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMTP Transport Driver
|
|
3
|
+
*
|
|
4
|
+
* Email transport using Nodemailer for SMTP servers.
|
|
5
|
+
*/
|
|
6
|
+
import type { MailTransport, SmtpConfig } from '../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Create an SMTP mail transport.
|
|
9
|
+
*
|
|
10
|
+
* @param config - SMTP configuration
|
|
11
|
+
* @returns Mail transport implementation
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const transport = await createSmtpTransport({
|
|
16
|
+
* host: 'smtp.gmail.com',
|
|
17
|
+
* port: 587,
|
|
18
|
+
* auth: {
|
|
19
|
+
* user: 'user@gmail.com',
|
|
20
|
+
* pass: 'app-password',
|
|
21
|
+
* },
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* await transport.send({
|
|
25
|
+
* from: { email: 'from@example.com', name: 'App' },
|
|
26
|
+
* to: [{ email: 'user@example.com' }],
|
|
27
|
+
* subject: 'Hello',
|
|
28
|
+
* html: '<h1>Hello World</h1>',
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function createSmtpTransport(config: SmtpConfig): Promise<MailTransport>;
|
|
33
|
+
/**
|
|
34
|
+
* SMTP transport driver name.
|
|
35
|
+
*/
|
|
36
|
+
export declare const DRIVER_NAME: "smtp";
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMTP Transport Driver
|
|
3
|
+
*
|
|
4
|
+
* Email transport using Nodemailer for SMTP servers.
|
|
5
|
+
*/
|
|
6
|
+
import { formatAddress, generateMessageId } from '../utils.js';
|
|
7
|
+
/**
|
|
8
|
+
* Default SMTP configuration.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_CONFIG = {
|
|
11
|
+
port: 587,
|
|
12
|
+
secure: false,
|
|
13
|
+
requireTLS: true, // Require TLS by default to prevent MITM downgrade attacks
|
|
14
|
+
connectionTimeout: 5000,
|
|
15
|
+
socketTimeout: 5000,
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Create an SMTP mail transport.
|
|
19
|
+
*
|
|
20
|
+
* @param config - SMTP configuration
|
|
21
|
+
* @returns Mail transport implementation
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* const transport = await createSmtpTransport({
|
|
26
|
+
* host: 'smtp.gmail.com',
|
|
27
|
+
* port: 587,
|
|
28
|
+
* auth: {
|
|
29
|
+
* user: 'user@gmail.com',
|
|
30
|
+
* pass: 'app-password',
|
|
31
|
+
* },
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* await transport.send({
|
|
35
|
+
* from: { email: 'from@example.com', name: 'App' },
|
|
36
|
+
* to: [{ email: 'user@example.com' }],
|
|
37
|
+
* subject: 'Hello',
|
|
38
|
+
* html: '<h1>Hello World</h1>',
|
|
39
|
+
* });
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export async function createSmtpTransport(config) {
|
|
43
|
+
const options = { ...DEFAULT_CONFIG, ...config };
|
|
44
|
+
// Dynamic import of nodemailer
|
|
45
|
+
const nodemailer = await import('nodemailer');
|
|
46
|
+
// Create transporter
|
|
47
|
+
const transporter = nodemailer.createTransport({
|
|
48
|
+
host: config.host,
|
|
49
|
+
port: options.port,
|
|
50
|
+
secure: options.secure,
|
|
51
|
+
requireTLS: options.requireTLS, // Fail if TLS upgrade not possible
|
|
52
|
+
auth: config.auth,
|
|
53
|
+
connectionTimeout: options.connectionTimeout,
|
|
54
|
+
socketTimeout: options.socketTimeout,
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* Convert EmailAddress array to nodemailer format.
|
|
58
|
+
*/
|
|
59
|
+
function formatAddresses(addresses) {
|
|
60
|
+
return addresses.map(formatAddress).join(', ');
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Convert attachments to nodemailer format.
|
|
64
|
+
*/
|
|
65
|
+
function formatAttachments(attachments) {
|
|
66
|
+
if (!attachments)
|
|
67
|
+
return [];
|
|
68
|
+
return attachments.map((att) => ({
|
|
69
|
+
filename: att.filename,
|
|
70
|
+
content: att.content,
|
|
71
|
+
contentType: att.contentType,
|
|
72
|
+
contentDisposition: att.disposition,
|
|
73
|
+
cid: att.cid,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
const transport = {
|
|
77
|
+
async send(sendOptions) {
|
|
78
|
+
try {
|
|
79
|
+
const messageId = generateMessageId();
|
|
80
|
+
const mailOptions = {
|
|
81
|
+
messageId,
|
|
82
|
+
from: formatAddress(sendOptions.from),
|
|
83
|
+
to: formatAddresses(sendOptions.to),
|
|
84
|
+
cc: sendOptions.cc ? formatAddresses(sendOptions.cc) : undefined,
|
|
85
|
+
bcc: sendOptions.bcc ? formatAddresses(sendOptions.bcc) : undefined,
|
|
86
|
+
replyTo: sendOptions.replyTo ? formatAddress(sendOptions.replyTo) : undefined,
|
|
87
|
+
subject: sendOptions.subject,
|
|
88
|
+
html: sendOptions.html,
|
|
89
|
+
text: sendOptions.text,
|
|
90
|
+
attachments: formatAttachments(sendOptions.attachments),
|
|
91
|
+
headers: sendOptions.headers,
|
|
92
|
+
};
|
|
93
|
+
const result = await transporter.sendMail(mailOptions);
|
|
94
|
+
return {
|
|
95
|
+
success: true,
|
|
96
|
+
messageId: result.messageId,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
101
|
+
return {
|
|
102
|
+
success: false,
|
|
103
|
+
error: errorMessage,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
async close() {
|
|
108
|
+
transporter.close();
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
return transport;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* SMTP transport driver name.
|
|
115
|
+
*/
|
|
116
|
+
export const DRIVER_NAME = 'smtp';
|