@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/types.d.ts
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mail Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for VeloxTS mail system.
|
|
5
|
+
*/
|
|
6
|
+
import type { ReactElement } from 'react';
|
|
7
|
+
import type { z } from 'zod';
|
|
8
|
+
/**
|
|
9
|
+
* Available mail transport drivers.
|
|
10
|
+
*/
|
|
11
|
+
export type MailDriver = 'smtp' | 'resend' | 'log';
|
|
12
|
+
/**
|
|
13
|
+
* Email address with optional name.
|
|
14
|
+
*/
|
|
15
|
+
export interface EmailAddress {
|
|
16
|
+
/**
|
|
17
|
+
* Email address (e.g., 'user@example.com').
|
|
18
|
+
*/
|
|
19
|
+
email: string;
|
|
20
|
+
/**
|
|
21
|
+
* Display name (e.g., 'John Doe').
|
|
22
|
+
*/
|
|
23
|
+
name?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Email recipient - either string or EmailAddress object.
|
|
27
|
+
*/
|
|
28
|
+
export type Recipient = string | EmailAddress;
|
|
29
|
+
/**
|
|
30
|
+
* Email attachment.
|
|
31
|
+
*/
|
|
32
|
+
export interface Attachment {
|
|
33
|
+
/**
|
|
34
|
+
* Filename to display.
|
|
35
|
+
*/
|
|
36
|
+
filename: string;
|
|
37
|
+
/**
|
|
38
|
+
* File content as Buffer or string.
|
|
39
|
+
*/
|
|
40
|
+
content: Buffer | string;
|
|
41
|
+
/**
|
|
42
|
+
* MIME type (e.g., 'application/pdf').
|
|
43
|
+
*/
|
|
44
|
+
contentType?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Content disposition (attachment or inline).
|
|
47
|
+
* @default 'attachment'
|
|
48
|
+
*/
|
|
49
|
+
disposition?: 'attachment' | 'inline';
|
|
50
|
+
/**
|
|
51
|
+
* Content ID for inline attachments.
|
|
52
|
+
*/
|
|
53
|
+
cid?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Email envelope for sending.
|
|
57
|
+
*/
|
|
58
|
+
export interface MailEnvelope {
|
|
59
|
+
/**
|
|
60
|
+
* Recipient(s) - required.
|
|
61
|
+
*/
|
|
62
|
+
to: Recipient | Recipient[];
|
|
63
|
+
/**
|
|
64
|
+
* Carbon copy recipient(s).
|
|
65
|
+
*/
|
|
66
|
+
cc?: Recipient | Recipient[];
|
|
67
|
+
/**
|
|
68
|
+
* Blind carbon copy recipient(s).
|
|
69
|
+
*/
|
|
70
|
+
bcc?: Recipient | Recipient[];
|
|
71
|
+
/**
|
|
72
|
+
* Reply-to address.
|
|
73
|
+
*/
|
|
74
|
+
replyTo?: Recipient;
|
|
75
|
+
/**
|
|
76
|
+
* Email attachments.
|
|
77
|
+
*/
|
|
78
|
+
attachments?: Attachment[];
|
|
79
|
+
/**
|
|
80
|
+
* Custom headers.
|
|
81
|
+
*/
|
|
82
|
+
headers?: Record<string, string>;
|
|
83
|
+
/**
|
|
84
|
+
* Tags for categorization (supported by some providers).
|
|
85
|
+
*/
|
|
86
|
+
tags?: string[];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Mail definition configuration.
|
|
90
|
+
*/
|
|
91
|
+
export interface MailDefinitionConfig<TSchema extends z.ZodType> {
|
|
92
|
+
/**
|
|
93
|
+
* Unique mail template name (e.g., 'welcome', 'password-reset').
|
|
94
|
+
*/
|
|
95
|
+
name: string;
|
|
96
|
+
/**
|
|
97
|
+
* Zod schema for template data validation.
|
|
98
|
+
*/
|
|
99
|
+
schema: TSchema;
|
|
100
|
+
/**
|
|
101
|
+
* Email subject - can be static string or function of data.
|
|
102
|
+
*/
|
|
103
|
+
subject: string | ((data: z.infer<TSchema>) => string);
|
|
104
|
+
/**
|
|
105
|
+
* React Email template component.
|
|
106
|
+
*/
|
|
107
|
+
template: (props: z.infer<TSchema>) => ReactElement;
|
|
108
|
+
/**
|
|
109
|
+
* Optional plain text version generator.
|
|
110
|
+
*/
|
|
111
|
+
text?: (data: z.infer<TSchema>) => string;
|
|
112
|
+
/**
|
|
113
|
+
* Default from address for this template.
|
|
114
|
+
*/
|
|
115
|
+
from?: Recipient;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Mail definition - type-safe email template.
|
|
119
|
+
*/
|
|
120
|
+
export interface MailDefinition<TSchema extends z.ZodType> {
|
|
121
|
+
/**
|
|
122
|
+
* Template name.
|
|
123
|
+
*/
|
|
124
|
+
readonly name: string;
|
|
125
|
+
/**
|
|
126
|
+
* Zod schema for data validation.
|
|
127
|
+
*/
|
|
128
|
+
readonly schema: TSchema;
|
|
129
|
+
/**
|
|
130
|
+
* Subject generator.
|
|
131
|
+
*/
|
|
132
|
+
readonly subject: string | ((data: z.infer<TSchema>) => string);
|
|
133
|
+
/**
|
|
134
|
+
* React Email template.
|
|
135
|
+
*/
|
|
136
|
+
readonly template: (props: z.infer<TSchema>) => ReactElement;
|
|
137
|
+
/**
|
|
138
|
+
* Plain text generator.
|
|
139
|
+
*/
|
|
140
|
+
readonly text?: (data: z.infer<TSchema>) => string;
|
|
141
|
+
/**
|
|
142
|
+
* Default from address.
|
|
143
|
+
*/
|
|
144
|
+
readonly from?: Recipient;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Options for sending an email.
|
|
148
|
+
*/
|
|
149
|
+
export interface SendMailOptions<TSchema extends z.ZodType> extends MailEnvelope {
|
|
150
|
+
/**
|
|
151
|
+
* Template data.
|
|
152
|
+
*/
|
|
153
|
+
data: z.infer<TSchema>;
|
|
154
|
+
/**
|
|
155
|
+
* Override from address.
|
|
156
|
+
*/
|
|
157
|
+
from?: Recipient;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Result of sending an email.
|
|
161
|
+
*/
|
|
162
|
+
export interface SendResult {
|
|
163
|
+
/**
|
|
164
|
+
* Whether the email was sent successfully.
|
|
165
|
+
*/
|
|
166
|
+
success: boolean;
|
|
167
|
+
/**
|
|
168
|
+
* Message ID from the transport.
|
|
169
|
+
*/
|
|
170
|
+
messageId?: string;
|
|
171
|
+
/**
|
|
172
|
+
* Error message if failed.
|
|
173
|
+
*/
|
|
174
|
+
error?: string;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* SMTP transport configuration.
|
|
178
|
+
*/
|
|
179
|
+
export interface SmtpConfig {
|
|
180
|
+
/**
|
|
181
|
+
* SMTP host.
|
|
182
|
+
*/
|
|
183
|
+
host: string;
|
|
184
|
+
/**
|
|
185
|
+
* SMTP port.
|
|
186
|
+
* @default 587
|
|
187
|
+
*/
|
|
188
|
+
port?: number;
|
|
189
|
+
/**
|
|
190
|
+
* Use implicit TLS/SSL (port 465).
|
|
191
|
+
* Set to true for port 465, false for port 587 with STARTTLS.
|
|
192
|
+
* @default false (uses STARTTLS on port 587)
|
|
193
|
+
*/
|
|
194
|
+
secure?: boolean;
|
|
195
|
+
/**
|
|
196
|
+
* Require TLS connection. When true, the connection will fail if
|
|
197
|
+
* STARTTLS upgrade is not possible. This prevents MITM downgrade attacks.
|
|
198
|
+
* Recommended for production use with port 587.
|
|
199
|
+
* @default true
|
|
200
|
+
*/
|
|
201
|
+
requireTLS?: boolean;
|
|
202
|
+
/**
|
|
203
|
+
* Authentication credentials.
|
|
204
|
+
*/
|
|
205
|
+
auth?: {
|
|
206
|
+
user: string;
|
|
207
|
+
pass: string;
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* Connection timeout in milliseconds.
|
|
211
|
+
* @default 5000
|
|
212
|
+
*/
|
|
213
|
+
connectionTimeout?: number;
|
|
214
|
+
/**
|
|
215
|
+
* Socket timeout in milliseconds.
|
|
216
|
+
* @default 5000
|
|
217
|
+
*/
|
|
218
|
+
socketTimeout?: number;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Resend transport configuration.
|
|
222
|
+
*/
|
|
223
|
+
export interface ResendConfig {
|
|
224
|
+
/**
|
|
225
|
+
* Resend API key.
|
|
226
|
+
*/
|
|
227
|
+
apiKey: string;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Log transport configuration (for development).
|
|
231
|
+
*/
|
|
232
|
+
export interface LogConfig {
|
|
233
|
+
/**
|
|
234
|
+
* Whether to output full HTML.
|
|
235
|
+
* @default false
|
|
236
|
+
*/
|
|
237
|
+
showHtml?: boolean;
|
|
238
|
+
/**
|
|
239
|
+
* Custom logger function.
|
|
240
|
+
*/
|
|
241
|
+
logger?: (message: string) => void;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Mail configuration by driver (discriminated union).
|
|
245
|
+
*/
|
|
246
|
+
export type MailConfig = {
|
|
247
|
+
driver: 'smtp';
|
|
248
|
+
config: SmtpConfig;
|
|
249
|
+
} | {
|
|
250
|
+
driver: 'resend';
|
|
251
|
+
config: ResendConfig;
|
|
252
|
+
} | {
|
|
253
|
+
driver: 'log';
|
|
254
|
+
config?: LogConfig;
|
|
255
|
+
};
|
|
256
|
+
/**
|
|
257
|
+
* Base options shared across all mail configurations.
|
|
258
|
+
*/
|
|
259
|
+
export interface MailBaseOptions {
|
|
260
|
+
/**
|
|
261
|
+
* Default from address for all emails.
|
|
262
|
+
*/
|
|
263
|
+
from?: Recipient;
|
|
264
|
+
/**
|
|
265
|
+
* Default reply-to address.
|
|
266
|
+
*/
|
|
267
|
+
replyTo?: Recipient;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Mail plugin options with SMTP driver.
|
|
271
|
+
*/
|
|
272
|
+
export interface MailSmtpOptions extends MailBaseOptions {
|
|
273
|
+
/**
|
|
274
|
+
* Mail transport driver.
|
|
275
|
+
*/
|
|
276
|
+
driver: 'smtp';
|
|
277
|
+
/**
|
|
278
|
+
* SMTP-specific configuration (required for SMTP driver).
|
|
279
|
+
*/
|
|
280
|
+
config: SmtpConfig;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Mail plugin options with Resend driver.
|
|
284
|
+
*/
|
|
285
|
+
export interface MailResendOptions extends MailBaseOptions {
|
|
286
|
+
/**
|
|
287
|
+
* Mail transport driver.
|
|
288
|
+
*/
|
|
289
|
+
driver: 'resend';
|
|
290
|
+
/**
|
|
291
|
+
* Resend-specific configuration (required for Resend driver).
|
|
292
|
+
*/
|
|
293
|
+
config: ResendConfig;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Mail plugin options with log driver.
|
|
297
|
+
*/
|
|
298
|
+
export interface MailLogOptions extends MailBaseOptions {
|
|
299
|
+
/**
|
|
300
|
+
* Mail transport driver.
|
|
301
|
+
*/
|
|
302
|
+
driver: 'log';
|
|
303
|
+
/**
|
|
304
|
+
* Log driver-specific configuration (optional).
|
|
305
|
+
*/
|
|
306
|
+
config?: LogConfig;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Mail plugin options with default driver (log).
|
|
310
|
+
* When no driver is specified, log is used as the default.
|
|
311
|
+
*/
|
|
312
|
+
export interface MailDefaultOptions extends MailBaseOptions {
|
|
313
|
+
/**
|
|
314
|
+
* Mail transport driver.
|
|
315
|
+
* @default 'log'
|
|
316
|
+
*/
|
|
317
|
+
driver?: undefined;
|
|
318
|
+
/**
|
|
319
|
+
* Log driver-specific configuration (default driver).
|
|
320
|
+
*/
|
|
321
|
+
config?: LogConfig;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Mail plugin options - discriminated union for type-safe driver configuration.
|
|
325
|
+
*
|
|
326
|
+
* The config type automatically narrows based on the selected driver:
|
|
327
|
+
* - `driver: 'smtp'` - config is SmtpConfig (required)
|
|
328
|
+
* - `driver: 'resend'` - config is ResendConfig (required)
|
|
329
|
+
* - `driver: 'log'` - config is LogConfig (optional)
|
|
330
|
+
* - no driver - defaults to log, config is LogConfig (optional)
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* ```typescript
|
|
334
|
+
* // SMTP driver
|
|
335
|
+
* mailPlugin({ driver: 'smtp', config: { host: 'smtp.example.com', auth: {...} } });
|
|
336
|
+
*
|
|
337
|
+
* // Resend driver
|
|
338
|
+
* mailPlugin({ driver: 'resend', config: { apiKey: 'key_...' } });
|
|
339
|
+
*
|
|
340
|
+
* // Log driver (for development)
|
|
341
|
+
* mailPlugin({ driver: 'log', config: { showHtml: true } });
|
|
342
|
+
*
|
|
343
|
+
* // Default (log)
|
|
344
|
+
* mailPlugin({ from: { email: 'test@example.com' } });
|
|
345
|
+
* ```
|
|
346
|
+
*/
|
|
347
|
+
export type MailPluginOptions = MailSmtpOptions | MailResendOptions | MailLogOptions | MailDefaultOptions;
|
|
348
|
+
/**
|
|
349
|
+
* Mail manager options - same discriminated union for standalone usage.
|
|
350
|
+
*/
|
|
351
|
+
export type MailManagerOptions = MailPluginOptions;
|
|
352
|
+
/**
|
|
353
|
+
* Mail transport interface for driver implementations.
|
|
354
|
+
*/
|
|
355
|
+
export interface MailTransport {
|
|
356
|
+
/**
|
|
357
|
+
* Send an email.
|
|
358
|
+
*/
|
|
359
|
+
send(options: {
|
|
360
|
+
from: EmailAddress;
|
|
361
|
+
to: EmailAddress[];
|
|
362
|
+
cc?: EmailAddress[];
|
|
363
|
+
bcc?: EmailAddress[];
|
|
364
|
+
replyTo?: EmailAddress;
|
|
365
|
+
subject: string;
|
|
366
|
+
html: string;
|
|
367
|
+
text?: string;
|
|
368
|
+
attachments?: Attachment[];
|
|
369
|
+
headers?: Record<string, string>;
|
|
370
|
+
tags?: string[];
|
|
371
|
+
}): Promise<SendResult>;
|
|
372
|
+
/**
|
|
373
|
+
* Close the transport connection.
|
|
374
|
+
*/
|
|
375
|
+
close(): Promise<void>;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Rendered email ready for sending.
|
|
379
|
+
*/
|
|
380
|
+
export interface RenderedMail {
|
|
381
|
+
/**
|
|
382
|
+
* From address.
|
|
383
|
+
*/
|
|
384
|
+
from: EmailAddress;
|
|
385
|
+
/**
|
|
386
|
+
* To addresses.
|
|
387
|
+
*/
|
|
388
|
+
to: EmailAddress[];
|
|
389
|
+
/**
|
|
390
|
+
* CC addresses.
|
|
391
|
+
*/
|
|
392
|
+
cc?: EmailAddress[];
|
|
393
|
+
/**
|
|
394
|
+
* BCC addresses.
|
|
395
|
+
*/
|
|
396
|
+
bcc?: EmailAddress[];
|
|
397
|
+
/**
|
|
398
|
+
* Reply-to address.
|
|
399
|
+
*/
|
|
400
|
+
replyTo?: EmailAddress;
|
|
401
|
+
/**
|
|
402
|
+
* Email subject.
|
|
403
|
+
*/
|
|
404
|
+
subject: string;
|
|
405
|
+
/**
|
|
406
|
+
* HTML body.
|
|
407
|
+
*/
|
|
408
|
+
html: string;
|
|
409
|
+
/**
|
|
410
|
+
* Plain text body.
|
|
411
|
+
*/
|
|
412
|
+
text?: string;
|
|
413
|
+
/**
|
|
414
|
+
* Attachments.
|
|
415
|
+
*/
|
|
416
|
+
attachments?: Attachment[];
|
|
417
|
+
/**
|
|
418
|
+
* Custom headers.
|
|
419
|
+
*/
|
|
420
|
+
headers?: Record<string, string>;
|
|
421
|
+
/**
|
|
422
|
+
* Tags.
|
|
423
|
+
*/
|
|
424
|
+
tags?: string[];
|
|
425
|
+
}
|
package/dist/types.js
ADDED
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mail Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for mail operations.
|
|
5
|
+
*/
|
|
6
|
+
import type { EmailAddress, Recipient } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Normalize a recipient to EmailAddress format.
|
|
9
|
+
*
|
|
10
|
+
* @param recipient - String email or EmailAddress object
|
|
11
|
+
* @returns Normalized EmailAddress
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* normalizeRecipient('user@example.com')
|
|
16
|
+
* // { email: 'user@example.com' }
|
|
17
|
+
*
|
|
18
|
+
* normalizeRecipient({ email: 'user@example.com', name: 'John' })
|
|
19
|
+
* // { email: 'user@example.com', name: 'John' }
|
|
20
|
+
*
|
|
21
|
+
* normalizeRecipient('John Doe <john@example.com>')
|
|
22
|
+
* // { email: 'john@example.com', name: 'John Doe' }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function normalizeRecipient(recipient: Recipient): EmailAddress;
|
|
26
|
+
/**
|
|
27
|
+
* Normalize multiple recipients.
|
|
28
|
+
*/
|
|
29
|
+
export declare function normalizeRecipients(recipients: Recipient | Recipient[]): EmailAddress[];
|
|
30
|
+
/**
|
|
31
|
+
* Sanitize a string to prevent email header injection.
|
|
32
|
+
* Removes newlines, carriage returns, and other control characters.
|
|
33
|
+
*
|
|
34
|
+
* @param value - String to sanitize
|
|
35
|
+
* @returns Sanitized string safe for email headers
|
|
36
|
+
*/
|
|
37
|
+
export declare function sanitizeHeaderValue(value: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Format an EmailAddress to string.
|
|
40
|
+
*
|
|
41
|
+
* @param address - EmailAddress to format
|
|
42
|
+
* @returns Formatted string (e.g., "John Doe <john@example.com>")
|
|
43
|
+
*/
|
|
44
|
+
export declare function formatAddress(address: EmailAddress): string;
|
|
45
|
+
/**
|
|
46
|
+
* Validate an email address format.
|
|
47
|
+
*
|
|
48
|
+
* Validates that:
|
|
49
|
+
* - Local part contains valid characters (alphanumeric, dots, plus, hyphens, underscores)
|
|
50
|
+
* - Domain contains valid characters (alphanumeric, dots, hyphens)
|
|
51
|
+
* - TLD has at least 2 characters
|
|
52
|
+
* - No consecutive dots
|
|
53
|
+
* - Doesn't start or end with special characters
|
|
54
|
+
*
|
|
55
|
+
* @param email - Email address to validate
|
|
56
|
+
* @returns True if valid
|
|
57
|
+
*/
|
|
58
|
+
export declare function isValidEmail(email: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Validate a recipient.
|
|
61
|
+
*
|
|
62
|
+
* @param recipient - Recipient to validate
|
|
63
|
+
* @throws Error if recipient is invalid
|
|
64
|
+
*/
|
|
65
|
+
export declare function validateRecipient(recipient: Recipient): void;
|
|
66
|
+
/**
|
|
67
|
+
* Validate multiple recipients.
|
|
68
|
+
*
|
|
69
|
+
* @param recipients - Recipients to validate
|
|
70
|
+
* @throws Error if any recipient is invalid
|
|
71
|
+
*/
|
|
72
|
+
export declare function validateRecipients(recipients: Recipient | Recipient[]): void;
|
|
73
|
+
/**
|
|
74
|
+
* Escape HTML entities for safe text output.
|
|
75
|
+
*/
|
|
76
|
+
export declare function escapeHtml(text: string): string;
|
|
77
|
+
/**
|
|
78
|
+
* Strip HTML tags from string.
|
|
79
|
+
*/
|
|
80
|
+
export declare function stripHtml(html: string): string;
|
|
81
|
+
/**
|
|
82
|
+
* Generate a unique message ID.
|
|
83
|
+
*/
|
|
84
|
+
export declare function generateMessageId(domain?: string): string;
|
|
85
|
+
/**
|
|
86
|
+
* Validate mail template name format.
|
|
87
|
+
* Template names should be kebab-case identifiers.
|
|
88
|
+
*/
|
|
89
|
+
export declare function validateTemplateName(name: string): void;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mail Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for mail operations.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Normalize a recipient to EmailAddress format.
|
|
8
|
+
*
|
|
9
|
+
* @param recipient - String email or EmailAddress object
|
|
10
|
+
* @returns Normalized EmailAddress
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* normalizeRecipient('user@example.com')
|
|
15
|
+
* // { email: 'user@example.com' }
|
|
16
|
+
*
|
|
17
|
+
* normalizeRecipient({ email: 'user@example.com', name: 'John' })
|
|
18
|
+
* // { email: 'user@example.com', name: 'John' }
|
|
19
|
+
*
|
|
20
|
+
* normalizeRecipient('John Doe <john@example.com>')
|
|
21
|
+
* // { email: 'john@example.com', name: 'John Doe' }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function normalizeRecipient(recipient) {
|
|
25
|
+
if (typeof recipient === 'object') {
|
|
26
|
+
return recipient;
|
|
27
|
+
}
|
|
28
|
+
// Parse "Name <email@example.com>" format
|
|
29
|
+
const match = recipient.match(/^(.+?)\s*<(.+?)>$/);
|
|
30
|
+
if (match) {
|
|
31
|
+
return {
|
|
32
|
+
name: match[1].trim(),
|
|
33
|
+
email: match[2].trim(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return { email: recipient };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Normalize multiple recipients.
|
|
40
|
+
*/
|
|
41
|
+
export function normalizeRecipients(recipients) {
|
|
42
|
+
const recipientArray = Array.isArray(recipients) ? recipients : [recipients];
|
|
43
|
+
return recipientArray.map(normalizeRecipient);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Sanitize a string to prevent email header injection.
|
|
47
|
+
* Removes newlines, carriage returns, and other control characters.
|
|
48
|
+
*
|
|
49
|
+
* @param value - String to sanitize
|
|
50
|
+
* @returns Sanitized string safe for email headers
|
|
51
|
+
*/
|
|
52
|
+
export function sanitizeHeaderValue(value) {
|
|
53
|
+
// Remove newlines, carriage returns, tabs, and other control characters
|
|
54
|
+
// that could be used for header injection attacks.
|
|
55
|
+
// Uses character-by-character filtering to avoid regex lint warnings.
|
|
56
|
+
let result = '';
|
|
57
|
+
for (const char of value) {
|
|
58
|
+
const code = char.charCodeAt(0);
|
|
59
|
+
// Replace control characters (0x00-0x1f) and DEL (0x7f) with space
|
|
60
|
+
if (code <= 0x1f || code === 0x7f) {
|
|
61
|
+
result += ' ';
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
result += char;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result.trim();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Format an EmailAddress to string.
|
|
71
|
+
*
|
|
72
|
+
* @param address - EmailAddress to format
|
|
73
|
+
* @returns Formatted string (e.g., "John Doe <john@example.com>")
|
|
74
|
+
*/
|
|
75
|
+
export function formatAddress(address) {
|
|
76
|
+
if (address.name) {
|
|
77
|
+
// Sanitize name to prevent header injection attacks
|
|
78
|
+
const sanitizedName = sanitizeHeaderValue(address.name);
|
|
79
|
+
return `${sanitizedName} <${address.email}>`;
|
|
80
|
+
}
|
|
81
|
+
return address.email;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Validate an email address format.
|
|
85
|
+
*
|
|
86
|
+
* Validates that:
|
|
87
|
+
* - Local part contains valid characters (alphanumeric, dots, plus, hyphens, underscores)
|
|
88
|
+
* - Domain contains valid characters (alphanumeric, dots, hyphens)
|
|
89
|
+
* - TLD has at least 2 characters
|
|
90
|
+
* - No consecutive dots
|
|
91
|
+
* - Doesn't start or end with special characters
|
|
92
|
+
*
|
|
93
|
+
* @param email - Email address to validate
|
|
94
|
+
* @returns True if valid
|
|
95
|
+
*/
|
|
96
|
+
export function isValidEmail(email) {
|
|
97
|
+
if (!email || typeof email !== 'string') {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
// More robust email validation:
|
|
101
|
+
// - Local part: letters, numbers, dots, plus, hyphens, underscores (no consecutive dots)
|
|
102
|
+
// - @ symbol
|
|
103
|
+
// - Domain: letters, numbers, dots, hyphens (no consecutive dots)
|
|
104
|
+
// - TLD: at least 2 letters
|
|
105
|
+
const emailRegex = /^[a-zA-Z0-9](?:[a-zA-Z0-9._+-]*[a-zA-Z0-9])?@[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$/;
|
|
106
|
+
// Additional check: no consecutive dots anywhere
|
|
107
|
+
if (email.includes('..')) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return emailRegex.test(email);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Validate a recipient.
|
|
114
|
+
*
|
|
115
|
+
* @param recipient - Recipient to validate
|
|
116
|
+
* @throws Error if recipient is invalid
|
|
117
|
+
*/
|
|
118
|
+
export function validateRecipient(recipient) {
|
|
119
|
+
const normalized = normalizeRecipient(recipient);
|
|
120
|
+
if (!normalized.email) {
|
|
121
|
+
throw new Error('Recipient email is required');
|
|
122
|
+
}
|
|
123
|
+
if (!isValidEmail(normalized.email)) {
|
|
124
|
+
throw new Error(`Invalid email address: ${normalized.email}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Validate multiple recipients.
|
|
129
|
+
*
|
|
130
|
+
* @param recipients - Recipients to validate
|
|
131
|
+
* @throws Error if any recipient is invalid
|
|
132
|
+
*/
|
|
133
|
+
export function validateRecipients(recipients) {
|
|
134
|
+
const recipientArray = Array.isArray(recipients) ? recipients : [recipients];
|
|
135
|
+
if (recipientArray.length === 0) {
|
|
136
|
+
throw new Error('At least one recipient is required');
|
|
137
|
+
}
|
|
138
|
+
for (const recipient of recipientArray) {
|
|
139
|
+
validateRecipient(recipient);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Escape HTML entities for safe text output.
|
|
144
|
+
*/
|
|
145
|
+
export function escapeHtml(text) {
|
|
146
|
+
const htmlEscapes = {
|
|
147
|
+
'&': '&',
|
|
148
|
+
'<': '<',
|
|
149
|
+
'>': '>',
|
|
150
|
+
'"': '"',
|
|
151
|
+
"'": ''',
|
|
152
|
+
};
|
|
153
|
+
return text.replace(/[&<>"']/g, (char) => htmlEscapes[char]);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Strip HTML tags from string.
|
|
157
|
+
*/
|
|
158
|
+
export function stripHtml(html) {
|
|
159
|
+
return html
|
|
160
|
+
.replace(/<[^>]*>/g, '') // Remove HTML tags
|
|
161
|
+
.replace(/ /g, ' ') // Replace with space
|
|
162
|
+
.replace(/&/g, '&') // Decode &
|
|
163
|
+
.replace(/</g, '<') // Decode <
|
|
164
|
+
.replace(/>/g, '>') // Decode >
|
|
165
|
+
.replace(/"/g, '"') // Decode "
|
|
166
|
+
.replace(/'/g, "'") // Decode '
|
|
167
|
+
.replace(/\s+/g, ' ') // Collapse whitespace
|
|
168
|
+
.trim();
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Generate a unique message ID.
|
|
172
|
+
*/
|
|
173
|
+
export function generateMessageId(domain) {
|
|
174
|
+
const timestamp = Date.now().toString(36);
|
|
175
|
+
const random = Math.random().toString(36).substring(2, 10);
|
|
176
|
+
const domainPart = domain ?? 'veloxts.local';
|
|
177
|
+
return `<${timestamp}.${random}@${domainPart}>`;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Validate mail template name format.
|
|
181
|
+
* Template names should be kebab-case identifiers.
|
|
182
|
+
*/
|
|
183
|
+
export function validateTemplateName(name) {
|
|
184
|
+
if (!name || typeof name !== 'string') {
|
|
185
|
+
throw new Error('Template name must be a non-empty string');
|
|
186
|
+
}
|
|
187
|
+
if (!/^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/.test(name)) {
|
|
188
|
+
throw new Error(`Invalid template name: ${name}. Use kebab-case format (e.g., 'welcome', 'password-reset')`);
|
|
189
|
+
}
|
|
190
|
+
}
|