@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.
@@ -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';