@zyther/tzmail 0.1.3 → 0.5.0

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 (132) hide show
  1. package/README.md +412 -79
  2. package/dist/core/enums/theme.enum.d.ts +11 -1
  3. package/dist/core/enums/theme.enum.d.ts.map +1 -1
  4. package/dist/core/enums/theme.enum.js +10 -0
  5. package/dist/core/interfaces/email.interface.d.ts +95 -5
  6. package/dist/core/interfaces/email.interface.d.ts.map +1 -1
  7. package/dist/core/interfaces/template.interface.d.ts +12 -1
  8. package/dist/core/interfaces/template.interface.d.ts.map +1 -1
  9. package/dist/factories/email-factory.d.ts +32 -6
  10. package/dist/factories/email-factory.d.ts.map +1 -1
  11. package/dist/factories/email-factory.js +110 -6
  12. package/dist/factories/template-factory.d.ts.map +1 -1
  13. package/dist/factories/template-factory.js +170 -60
  14. package/dist/index.d.ts +16 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +23 -1
  17. package/dist/providers/mailgun.transport.d.ts +10 -0
  18. package/dist/providers/mailgun.transport.d.ts.map +1 -0
  19. package/dist/providers/mailgun.transport.js +85 -0
  20. package/dist/providers/nodemailer.transport.d.ts +11 -0
  21. package/dist/providers/nodemailer.transport.d.ts.map +1 -0
  22. package/dist/providers/nodemailer.transport.js +23 -0
  23. package/dist/providers/provider-utils.d.ts +17 -0
  24. package/dist/providers/provider-utils.d.ts.map +1 -0
  25. package/dist/providers/provider-utils.js +89 -0
  26. package/dist/providers/provider.interface.d.ts +30 -0
  27. package/dist/providers/provider.interface.d.ts.map +1 -0
  28. package/dist/providers/provider.interface.js +2 -0
  29. package/dist/providers/sendgrid.transport.d.ts +10 -0
  30. package/dist/providers/sendgrid.transport.d.ts.map +1 -0
  31. package/dist/providers/sendgrid.transport.js +76 -0
  32. package/dist/queues/bullmq-email-queue.d.ts +153 -0
  33. package/dist/queues/bullmq-email-queue.d.ts.map +1 -0
  34. package/dist/queues/bullmq-email-queue.js +292 -0
  35. package/dist/services/attachment.service.d.ts +15 -37
  36. package/dist/services/attachment.service.d.ts.map +1 -1
  37. package/dist/services/attachment.service.js +58 -46
  38. package/dist/services/email.service.d.ts +22 -22
  39. package/dist/services/email.service.d.ts.map +1 -1
  40. package/dist/services/email.service.js +117 -33
  41. package/dist/services/template.service.d.ts +24 -111
  42. package/dist/services/template.service.d.ts.map +1 -1
  43. package/dist/services/template.service.js +273 -362
  44. package/dist/templates/base/color-accessibility.d.ts +4 -0
  45. package/dist/templates/base/color-accessibility.d.ts.map +1 -0
  46. package/dist/templates/base/color-accessibility.js +38 -0
  47. package/dist/templates/base/template-builder.d.ts +4 -1
  48. package/dist/templates/base/template-builder.d.ts.map +1 -1
  49. package/dist/templates/base/template-builder.js +276 -527
  50. package/dist/templates/themes/aurora.theme.d.ts +12 -0
  51. package/dist/templates/themes/aurora.theme.d.ts.map +1 -0
  52. package/dist/templates/themes/aurora.theme.js +88 -0
  53. package/dist/templates/themes/corporate.theme.d.ts +2 -1
  54. package/dist/templates/themes/corporate.theme.d.ts.map +1 -1
  55. package/dist/templates/themes/corporate.theme.js +139 -102
  56. package/dist/templates/themes/editorial.theme.d.ts +12 -0
  57. package/dist/templates/themes/editorial.theme.d.ts.map +1 -0
  58. package/dist/templates/themes/editorial.theme.js +90 -0
  59. package/dist/templates/themes/forest.theme.d.ts +12 -0
  60. package/dist/templates/themes/forest.theme.d.ts.map +1 -0
  61. package/dist/templates/themes/forest.theme.js +89 -0
  62. package/dist/templates/themes/glass.theme.d.ts +12 -0
  63. package/dist/templates/themes/glass.theme.d.ts.map +1 -0
  64. package/dist/templates/themes/glass.theme.js +88 -0
  65. package/dist/templates/themes/lavender.theme.d.ts +12 -0
  66. package/dist/templates/themes/lavender.theme.d.ts.map +1 -0
  67. package/dist/templates/themes/lavender.theme.js +91 -0
  68. package/dist/templates/themes/midnight.theme.d.ts +12 -0
  69. package/dist/templates/themes/midnight.theme.d.ts.map +1 -0
  70. package/dist/templates/themes/midnight.theme.js +91 -0
  71. package/dist/templates/themes/minimal.theme.d.ts +2 -1
  72. package/dist/templates/themes/minimal.theme.d.ts.map +1 -1
  73. package/dist/templates/themes/minimal.theme.js +142 -107
  74. package/dist/templates/themes/modern.theme.d.ts +2 -1
  75. package/dist/templates/themes/modern.theme.d.ts.map +1 -1
  76. package/dist/templates/themes/modern.theme.js +109 -74
  77. package/dist/templates/themes/monokai.theme.d.ts +2 -1
  78. package/dist/templates/themes/monokai.theme.d.ts.map +1 -1
  79. package/dist/templates/themes/monokai.theme.js +102 -66
  80. package/dist/templates/themes/neobrutal.theme.d.ts +12 -0
  81. package/dist/templates/themes/neobrutal.theme.d.ts.map +1 -0
  82. package/dist/templates/themes/neobrutal.theme.js +91 -0
  83. package/dist/templates/themes/ocean.theme.d.ts +12 -0
  84. package/dist/templates/themes/ocean.theme.d.ts.map +1 -0
  85. package/dist/templates/themes/ocean.theme.js +90 -0
  86. package/dist/templates/themes/pastel.theme.d.ts +12 -0
  87. package/dist/templates/themes/pastel.theme.d.ts.map +1 -0
  88. package/dist/templates/themes/pastel.theme.js +89 -0
  89. package/dist/templates/themes/sunset.theme.d.ts +12 -0
  90. package/dist/templates/themes/sunset.theme.d.ts.map +1 -0
  91. package/dist/templates/themes/sunset.theme.js +90 -0
  92. package/dist/templates/themes/system.theme.d.ts +2 -1
  93. package/dist/templates/themes/system.theme.d.ts.map +1 -1
  94. package/dist/templates/themes/system.theme.js +81 -47
  95. package/dist/templates/themes/theme.interface.d.ts +48 -0
  96. package/dist/templates/themes/theme.interface.d.ts.map +1 -1
  97. package/package.json +36 -14
  98. package/src/core/enums/template-part.enum.ts +7 -0
  99. package/src/core/enums/theme.enum.ts +18 -0
  100. package/src/core/interfaces/email.interface.ts +116 -0
  101. package/src/core/interfaces/template.interface.ts +71 -0
  102. package/src/factories/email-factory.ts +208 -0
  103. package/src/factories/template-factory.ts +201 -0
  104. package/src/index.ts +53 -0
  105. package/src/providers/mailgun.transport.ts +80 -0
  106. package/src/providers/nodemailer.transport.ts +20 -0
  107. package/src/providers/provider-utils.ts +91 -0
  108. package/src/providers/provider.interface.ts +26 -0
  109. package/src/providers/sendgrid.transport.ts +80 -0
  110. package/src/queues/bullmq-email-queue.ts +418 -0
  111. package/src/services/attachment.service.ts +98 -0
  112. package/src/services/email.service.ts +151 -0
  113. package/src/services/template.service.ts +431 -0
  114. package/src/templates/base/color-accessibility.ts +31 -0
  115. package/src/templates/base/template-builder.ts +438 -0
  116. package/src/templates/themes/aurora.theme.ts +91 -0
  117. package/src/templates/themes/corporate.theme.ts +156 -0
  118. package/src/templates/themes/editorial.theme.ts +93 -0
  119. package/src/templates/themes/forest.theme.ts +92 -0
  120. package/src/templates/themes/glass.theme.ts +91 -0
  121. package/src/templates/themes/lavender.theme.ts +94 -0
  122. package/src/templates/themes/midnight.theme.ts +94 -0
  123. package/src/templates/themes/minimal.theme.ts +158 -0
  124. package/src/templates/themes/modern.theme.ts +124 -0
  125. package/src/templates/themes/monokai.theme.ts +117 -0
  126. package/src/templates/themes/neobrutal.theme.ts +94 -0
  127. package/src/templates/themes/ocean.theme.ts +93 -0
  128. package/src/templates/themes/pastel.theme.ts +92 -0
  129. package/src/templates/themes/sunset.theme.ts +93 -0
  130. package/src/templates/themes/system.theme.ts +92 -0
  131. package/src/templates/themes/theme.interface.ts +95 -0
  132. package/src/tests/template.service.test.ts +1 -0
@@ -0,0 +1,30 @@
1
+ import { IAttachment } from '../core/interfaces/email.interface';
2
+ export interface ITransportMessage {
3
+ from?: string;
4
+ to: string | string[];
5
+ cc?: string | string[];
6
+ bcc?: string | string[];
7
+ subject: string;
8
+ text?: string;
9
+ html?: string;
10
+ attachments?: IAttachment[];
11
+ }
12
+ export interface ITransportSendInfo {
13
+ messageId?: string;
14
+ response?: string;
15
+ accepted?: Array<string | {
16
+ address: string;
17
+ name: string;
18
+ }>;
19
+ rejected?: Array<string | {
20
+ address: string;
21
+ name: string;
22
+ }>;
23
+ }
24
+ /** Small common contract; provider transports may omit verify when their API has no safe health check. */
25
+ export interface IEmailTransport {
26
+ sendMail(message: ITransportMessage): Promise<ITransportSendInfo>;
27
+ verify?(): Promise<true>;
28
+ close?(): void;
29
+ }
30
+ //# sourceMappingURL=provider.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.interface.d.ts","sourceRoot":"","sources":["../../src/providers/provider.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAEjE,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACtB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,WAAW,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,QAAQ,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9D;AAED,0GAA0G;AAC1G,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAClE,MAAM,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,KAAK,CAAC,IAAI,IAAI,CAAC;CAChB"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,10 @@
1
+ import { ISendGridConfig } from '../core/interfaces/email.interface';
2
+ import { ITransportMessage, ITransportSendInfo, IEmailTransport } from './provider.interface';
3
+ export declare class SendGridTransport implements IEmailTransport {
4
+ private readonly config;
5
+ private readonly apiUrl;
6
+ private readonly fetchImpl;
7
+ constructor(config: ISendGridConfig, fetchImpl?: typeof fetch);
8
+ sendMail(message: ITransportMessage): Promise<ITransportSendInfo>;
9
+ }
10
+ //# sourceMappingURL=sendgrid.transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sendgrid.transport.d.ts","sourceRoot":"","sources":["../../src/providers/sendgrid.transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG9F,qBAAa,iBAAkB,YAAW,eAAe;IAI3C,OAAO,CAAC,QAAQ,CAAC,MAAM;IAHnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;gBAEZ,MAAM,EAAE,eAAe,EAAE,SAAS,GAAE,OAAO,KAAa;IAU/E,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC;CA6DxE"}
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SendGridTransport = void 0;
4
+ const provider_utils_1 = require("./provider-utils");
5
+ class SendGridTransport {
6
+ config;
7
+ apiUrl;
8
+ fetchImpl;
9
+ constructor(config, fetchImpl = fetch) {
10
+ this.config = config;
11
+ (0, provider_utils_1.validateApiConfig)(config);
12
+ this.apiUrl = (config.apiUrl ?? 'https://api.sendgrid.com').replace(/\/+$/, '');
13
+ const parsedUrl = new URL(this.apiUrl);
14
+ if (parsedUrl.protocol !== 'https:' && parsedUrl.hostname !== 'localhost' && parsedUrl.hostname !== '127.0.0.1') {
15
+ throw new Error('SendGrid apiUrl must use HTTPS');
16
+ }
17
+ this.fetchImpl = fetchImpl;
18
+ }
19
+ async sendMail(message) {
20
+ const recipients = (0, provider_utils_1.parseMailboxes)(message.to);
21
+ if (recipients.length === 0)
22
+ throw new Error('SendGrid requires at least one recipient');
23
+ const from = (0, provider_utils_1.parseMailboxes)(message.from ?? this.config.defaultFrom)[0];
24
+ if (!from)
25
+ throw new Error('SendGrid requires a sender');
26
+ const content = [
27
+ ...(message.text !== undefined ? [{ type: 'text/plain', value: message.text }] : []),
28
+ ...(message.html !== undefined ? [{ type: 'text/html', value: message.html }] : [])
29
+ ];
30
+ if (content.length === 0)
31
+ throw new Error('SendGrid requires a text or HTML body');
32
+ const attachments = await Promise.all((message.attachments ?? []).map(async (attachment) => ({
33
+ content: (await (0, provider_utils_1.attachmentToBuffer)(attachment, this.config.maxAttachmentBytes)).toString('base64'),
34
+ filename: attachment.filename,
35
+ type: attachment.contentType ?? 'application/octet-stream',
36
+ disposition: attachment.cid ? 'inline' : 'attachment',
37
+ ...(attachment.cid ? { content_id: attachment.cid } : {})
38
+ })));
39
+ const payload = {
40
+ personalizations: [{
41
+ to: recipients,
42
+ ...(message.cc ? { cc: (0, provider_utils_1.parseMailboxes)(message.cc) } : {}),
43
+ ...(message.bcc ? { bcc: (0, provider_utils_1.parseMailboxes)(message.bcc) } : {})
44
+ }],
45
+ from,
46
+ subject: message.subject,
47
+ content,
48
+ ...(attachments.length > 0 ? { attachments } : {})
49
+ };
50
+ const response = await this.fetchImpl(`${this.apiUrl}/v3/mail/send`, {
51
+ method: 'POST',
52
+ headers: {
53
+ authorization: `Bearer ${this.config.apiKey}`,
54
+ 'content-type': 'application/json'
55
+ },
56
+ body: JSON.stringify(payload),
57
+ redirect: 'error',
58
+ signal: AbortSignal.timeout(this.config.requestTimeoutMs ?? 30_000)
59
+ });
60
+ if (response.status !== 202) {
61
+ const responseBody = await response.text();
62
+ const error = new Error(`SendGrid API returned HTTP ${response.status}${responseBody ? `: ${responseBody}` : ''}`);
63
+ Object.assign(error, { statusCode: response.status, responseBody });
64
+ throw error;
65
+ }
66
+ const accepted = [...recipients, ...(0, provider_utils_1.parseMailboxes)(message.cc), ...(0, provider_utils_1.parseMailboxes)(message.bcc)].map(({ email, name }) => name ? { address: email, name } : email);
67
+ const messageId = response.headers.get('x-message-id') ?? undefined;
68
+ return {
69
+ ...(messageId ? { messageId } : {}),
70
+ response: '202 Accepted',
71
+ accepted,
72
+ rejected: []
73
+ };
74
+ }
75
+ }
76
+ exports.SendGridTransport = SendGridTransport;
@@ -0,0 +1,153 @@
1
+ import { IAttachment, IEmailOptions, IEmailSendResult } from '../core/interfaces/email.interface';
2
+ export interface IBullMQBackoffOptions {
3
+ type: 'fixed' | 'exponential';
4
+ delay: number;
5
+ jitter?: number;
6
+ }
7
+ export interface IEmailQueueJobOptions {
8
+ /** Stable business key. It is hashed before being used as a Redis job ID. */
9
+ idempotencyKey?: string;
10
+ delay?: number;
11
+ priority?: number;
12
+ attempts?: number;
13
+ backoff?: IBullMQBackoffOptions;
14
+ }
15
+ export interface IBullMQRateLimiterOptions {
16
+ max: number;
17
+ duration: number;
18
+ }
19
+ export interface IBullMQWorkerMetricsOptions {
20
+ /** Number of one-minute data points retained in Redis for completed/failed jobs. */
21
+ maxDataPoints: number;
22
+ }
23
+ export interface IBullMQEmailQueueConfig {
24
+ /** Redis/ioredis connection options. Keep credentials in a secret manager. */
25
+ connection: Record<string, unknown>;
26
+ queueName?: string;
27
+ prefix?: string;
28
+ concurrency?: number;
29
+ limiter?: IBullMQRateLimiterOptions;
30
+ workerMetrics?: IBullMQWorkerMetricsOptions;
31
+ maxPayloadBytes?: number;
32
+ maxBulkSize?: number;
33
+ defaultJobOptions?: {
34
+ attempts?: number;
35
+ backoff?: IBullMQBackoffOptions;
36
+ removeOnComplete?: boolean | number | {
37
+ age?: number;
38
+ count?: number;
39
+ };
40
+ removeOnFail?: boolean | number | {
41
+ age?: number;
42
+ count?: number;
43
+ };
44
+ };
45
+ }
46
+ export interface IEmailQueueSender {
47
+ sendEmail(options: IEmailOptions): Promise<IEmailSendResult>;
48
+ }
49
+ export interface IQueuedEmailJob {
50
+ id?: string;
51
+ }
52
+ export type ISerializedEmailAttachment = Omit<IAttachment, 'content'> & {
53
+ contentBase64?: string;
54
+ };
55
+ export interface IEmailQueueJobData {
56
+ version: 1;
57
+ email: Omit<IEmailOptions, 'template' | 'templateData' | 'attachments'> & {
58
+ attachments?: ISerializedEmailAttachment[];
59
+ };
60
+ }
61
+ export interface IBullMQJobLike {
62
+ id?: string | number;
63
+ name: string;
64
+ data: IEmailQueueJobData;
65
+ }
66
+ export interface IBullMQQueueMetrics {
67
+ data: number[];
68
+ count: number;
69
+ meta: {
70
+ count: number;
71
+ prevTS: number;
72
+ prevCount: number;
73
+ };
74
+ }
75
+ export interface IBullMQQueueEventPayload {
76
+ jobId?: string;
77
+ prev?: string;
78
+ failedReason?: string;
79
+ returnvalue?: unknown;
80
+ data?: unknown;
81
+ [key: string]: unknown;
82
+ }
83
+ export interface IBullMQQueueEventsLike {
84
+ on(event: string, listener: (payload: IBullMQQueueEventPayload) => void): this;
85
+ close(): Promise<void>;
86
+ }
87
+ interface IBullMQQueueLike {
88
+ add(name: string, data: IEmailQueueJobData, options?: Record<string, unknown>): Promise<IQueuedEmailJob>;
89
+ addBulk(jobs: Array<{
90
+ name: string;
91
+ data: IEmailQueueJobData;
92
+ opts?: Record<string, unknown>;
93
+ }>): Promise<IQueuedEmailJob[]>;
94
+ close(): Promise<void>;
95
+ pause(): Promise<void>;
96
+ resume(): Promise<void>;
97
+ getJobCounts(...types: string[]): Promise<Record<string, number>>;
98
+ exportPrometheusMetrics?(globalVariables?: Record<string, string>): Promise<string>;
99
+ getMetrics?(type: 'completed' | 'failed', start?: number, end?: number): Promise<IBullMQQueueMetrics>;
100
+ }
101
+ interface IBullMQWorkerLike {
102
+ close(): Promise<void>;
103
+ }
104
+ export interface IBullMQRuntime {
105
+ Queue: new (name: string, options: Record<string, unknown>) => IBullMQQueueLike;
106
+ Worker: new (name: string, processor: (job: IBullMQJobLike) => Promise<IEmailSendResult>, options: Record<string, unknown>) => IBullMQWorkerLike;
107
+ UnrecoverableError: new (message: string) => Error;
108
+ QueueEvents?: new (name: string, options: Record<string, unknown>) => IBullMQQueueEventsLike;
109
+ }
110
+ /**
111
+ * Redis-backed asynchronous email delivery using BullMQ. This class may be
112
+ * instantiated in producer and worker processes with the same queue settings.
113
+ */
114
+ export declare class BullMQEmailQueue {
115
+ private readonly config;
116
+ readonly queueName: string;
117
+ private readonly runtime;
118
+ private readonly queue;
119
+ private readonly workers;
120
+ private readonly queueEvents;
121
+ private readonly maxPayloadBytes;
122
+ private readonly maxBulkSize;
123
+ private closed;
124
+ constructor(config: IBullMQEmailQueueConfig, runtime?: IBullMQRuntime);
125
+ /** Add one email job. Template rendering happens before enqueue so jobs stay serializable. */
126
+ enqueueEmail(options: IEmailOptions, job?: IEmailQueueJobOptions): Promise<IQueuedEmailJob>;
127
+ /** Add a bounded batch with one Redis bulk operation. BullMQ/Redis bulk calls can partially add jobs on failure. */
128
+ enqueueBulk(items: Array<{
129
+ email: IEmailOptions;
130
+ job?: IEmailQueueJobOptions;
131
+ }>): Promise<IQueuedEmailJob[]>;
132
+ /** Start a BullMQ Worker. Run this in dedicated worker processes for horizontal scaling. */
133
+ startWorker(sender: IEmailQueueSender, options?: {
134
+ concurrency?: number;
135
+ limiter?: IBullMQRateLimiterOptions;
136
+ metrics?: IBullMQWorkerMetricsOptions;
137
+ }): IBullMQWorkerLike;
138
+ pause(): Promise<void>;
139
+ resume(): Promise<void>;
140
+ getJobCounts(): Promise<Record<string, number>>;
141
+ /** Export queue counts in Prometheus text exposition format. */
142
+ exportPrometheusMetrics(globalVariables?: Record<string, string>): Promise<string>;
143
+ /** Read BullMQ's per-minute completed/failed worker metrics from Redis. */
144
+ getMetrics(type: 'completed' | 'failed', start?: number, end?: number): Promise<IBullMQQueueMetrics>;
145
+ /** Listen to lifecycle events across all workers using BullMQ QueueEvents. */
146
+ createQueueEvents(): IBullMQQueueEventsLike;
147
+ /** Gracefully wait for active workers and release the BullMQ queue connection. */
148
+ close(): Promise<void>;
149
+ private serializeEmail;
150
+ private assertOpen;
151
+ }
152
+ export {};
153
+ //# sourceMappingURL=bullmq-email-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bullmq-email-queue.d.ts","sourceRoot":"","sources":["../../src/queues/bullmq-email-queue.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,292 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BullMQEmailQueue = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const module_1 = require("module");
6
+ const loadOptionalModule = (0, module_1.createRequire)(__filename);
7
+ function loadBullMQRuntime() {
8
+ try {
9
+ return loadOptionalModule('bullmq');
10
+ }
11
+ catch (error) {
12
+ throw new Error('BullMQ support requires the optional package bullmq. Install it with: npm install bullmq', { cause: error });
13
+ }
14
+ }
15
+ function assertPositiveInteger(name, value) {
16
+ if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0)) {
17
+ throw new Error(`${name} must be a positive safe integer`);
18
+ }
19
+ }
20
+ function assertJobOptions(options) {
21
+ assertPositiveInteger('attempts', options.attempts);
22
+ if (options.delay !== undefined && (!Number.isSafeInteger(options.delay) || options.delay < 0)) {
23
+ throw new Error('delay must be a non-negative safe integer');
24
+ }
25
+ if (options.priority !== undefined && (!Number.isSafeInteger(options.priority) || options.priority < 0)) {
26
+ throw new Error('priority must be a non-negative safe integer');
27
+ }
28
+ if (options.backoff) {
29
+ assertPositiveInteger('backoff.delay', options.backoff.delay);
30
+ if (options.backoff.jitter !== undefined && (options.backoff.jitter < 0 || options.backoff.jitter > 1)) {
31
+ throw new Error('backoff.jitter must be between 0 and 1');
32
+ }
33
+ }
34
+ if (options.idempotencyKey !== undefined && !options.idempotencyKey.trim()) {
35
+ throw new Error('idempotencyKey must not be empty');
36
+ }
37
+ }
38
+ function jobOptions(options) {
39
+ const { idempotencyKey, ...bullOptions } = options;
40
+ return {
41
+ ...bullOptions,
42
+ ...(idempotencyKey ? { jobId: `email-${(0, crypto_1.createHash)('sha256').update(idempotencyKey).digest('hex')}` } : {})
43
+ };
44
+ }
45
+ function serializeAttachment(attachment) {
46
+ const { content, ...metadata } = attachment;
47
+ if (content === undefined)
48
+ return metadata;
49
+ const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content);
50
+ return { ...metadata, contentBase64: buffer.toString('base64') };
51
+ }
52
+ function deserializeEmail(data) {
53
+ if (data?.version !== 1 || !data.email || typeof data.email !== 'object') {
54
+ throw new Error('Unsupported or malformed tzmail queue job payload');
55
+ }
56
+ const { attachments, ...message } = data.email;
57
+ return {
58
+ ...message,
59
+ ...(attachments ? {
60
+ attachments: attachments.map(({ contentBase64, ...attachment }) => ({
61
+ ...attachment,
62
+ ...(contentBase64 !== undefined ? { content: Buffer.from(contentBase64, 'base64') } : {})
63
+ }))
64
+ } : {})
65
+ };
66
+ }
67
+ /**
68
+ * Redis-backed asynchronous email delivery using BullMQ. This class may be
69
+ * instantiated in producer and worker processes with the same queue settings.
70
+ */
71
+ class BullMQEmailQueue {
72
+ config;
73
+ queueName;
74
+ runtime;
75
+ queue;
76
+ workers = new Set();
77
+ queueEvents = new Set();
78
+ maxPayloadBytes;
79
+ maxBulkSize;
80
+ closed = false;
81
+ constructor(config, runtime) {
82
+ this.config = config;
83
+ if (!config.connection || typeof config.connection !== 'object') {
84
+ throw new Error('BullMQ Redis connection options are required');
85
+ }
86
+ this.queueName = config.queueName ?? 'tzmail-email';
87
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(this.queueName)) {
88
+ throw new Error('queueName must contain only letters, numbers, underscores or hyphens, with a maximum length of 128');
89
+ }
90
+ assertPositiveInteger('concurrency', config.concurrency);
91
+ assertPositiveInteger('maxPayloadBytes', config.maxPayloadBytes);
92
+ assertPositiveInteger('maxBulkSize', config.maxBulkSize);
93
+ assertPositiveInteger('defaultJobOptions.attempts', config.defaultJobOptions?.attempts);
94
+ assertPositiveInteger('workerMetrics.maxDataPoints', config.workerMetrics?.maxDataPoints);
95
+ if (config.defaultJobOptions?.backoff)
96
+ assertJobOptions({ backoff: config.defaultJobOptions.backoff });
97
+ if (config.limiter) {
98
+ assertPositiveInteger('limiter.max', config.limiter.max);
99
+ assertPositiveInteger('limiter.duration', config.limiter.duration);
100
+ }
101
+ this.maxPayloadBytes = config.maxPayloadBytes ?? 10 * 1024 * 1024;
102
+ this.maxBulkSize = config.maxBulkSize ?? 1_000;
103
+ this.runtime = runtime ?? loadBullMQRuntime();
104
+ this.queue = new this.runtime.Queue(this.queueName, {
105
+ connection: config.connection,
106
+ ...(config.prefix ? { prefix: config.prefix } : {}),
107
+ defaultJobOptions: {
108
+ attempts: 5,
109
+ backoff: { type: 'exponential', delay: 1_000, jitter: 0.2 },
110
+ removeOnComplete: { age: 86_400, count: 10_000 },
111
+ removeOnFail: { age: 604_800, count: 50_000 },
112
+ ...config.defaultJobOptions
113
+ }
114
+ });
115
+ }
116
+ /** Add one email job. Template rendering happens before enqueue so jobs stay serializable. */
117
+ async enqueueEmail(options, job = {}) {
118
+ this.assertOpen();
119
+ assertJobOptions(job);
120
+ const data = await this.serializeEmail(options);
121
+ return this.queue.add('send-email', data, jobOptions(job));
122
+ }
123
+ /** Add a bounded batch with one Redis bulk operation. BullMQ/Redis bulk calls can partially add jobs on failure. */
124
+ async enqueueBulk(items) {
125
+ this.assertOpen();
126
+ if (!Array.isArray(items))
127
+ throw new Error('items must be an array');
128
+ if (items.length === 0)
129
+ return [];
130
+ if (items.length > this.maxBulkSize) {
131
+ throw new Error(`Bulk exceeds configured maxBulkSize (${this.maxBulkSize})`);
132
+ }
133
+ if (items.some((item) => !item || !item.email)) {
134
+ throw new Error('Each bulk item requires an email message');
135
+ }
136
+ const keys = items.map((item) => item.job?.idempotencyKey).filter((key) => key !== undefined);
137
+ if (new Set(keys).size !== keys.length) {
138
+ throw new Error('idempotencyKey values must be unique within one bulk request');
139
+ }
140
+ const jobs = await Promise.all(items.map(async (item) => {
141
+ const options = item.job ?? {};
142
+ assertJobOptions(options);
143
+ return {
144
+ name: 'send-email',
145
+ data: await this.serializeEmail(item.email),
146
+ opts: jobOptions(options)
147
+ };
148
+ }));
149
+ return this.queue.addBulk(jobs);
150
+ }
151
+ /** Start a BullMQ Worker. Run this in dedicated worker processes for horizontal scaling. */
152
+ startWorker(sender, options = {}) {
153
+ this.assertOpen();
154
+ const concurrency = options.concurrency ?? this.config.concurrency ?? 5;
155
+ assertPositiveInteger('concurrency', concurrency);
156
+ const limiter = options.limiter ?? this.config.limiter;
157
+ const metrics = options.metrics ?? this.config.workerMetrics;
158
+ if (limiter) {
159
+ assertPositiveInteger('limiter.max', limiter.max);
160
+ assertPositiveInteger('limiter.duration', limiter.duration);
161
+ }
162
+ assertPositiveInteger('metrics.maxDataPoints', metrics?.maxDataPoints);
163
+ const worker = new this.runtime.Worker(this.queueName, async (job) => {
164
+ let email;
165
+ try {
166
+ email = deserializeEmail(job.data);
167
+ }
168
+ catch (error) {
169
+ throw new this.runtime.UnrecoverableError(error instanceof Error ? error.message : 'Malformed email job payload');
170
+ }
171
+ const result = await sender.sendEmail(email);
172
+ if (result.status === 'success')
173
+ return result;
174
+ if (result.status === 'partial') {
175
+ throw new this.runtime.UnrecoverableError('Email was accepted for only some recipients; automatic retry is disabled to avoid duplicate delivery.');
176
+ }
177
+ if (result.errorStage === 'validation' || result.errorStage === 'render') {
178
+ throw new this.runtime.UnrecoverableError(`Email job failed at ${result.errorStage} stage.`);
179
+ }
180
+ if (result.error instanceof Error)
181
+ throw result.error;
182
+ throw new Error(`Email job failed at ${result.errorStage ?? 'unknown'} stage.`);
183
+ }, {
184
+ connection: this.config.connection,
185
+ ...(this.config.prefix ? { prefix: this.config.prefix } : {}),
186
+ concurrency,
187
+ ...(limiter ? { limiter } : {}),
188
+ ...(metrics ? { metrics } : {})
189
+ });
190
+ this.workers.add(worker);
191
+ return worker;
192
+ }
193
+ pause() {
194
+ this.assertOpen();
195
+ return this.queue.pause();
196
+ }
197
+ resume() {
198
+ this.assertOpen();
199
+ return this.queue.resume();
200
+ }
201
+ getJobCounts() {
202
+ this.assertOpen();
203
+ return this.queue.getJobCounts('wait', 'active', 'completed', 'failed', 'delayed', 'paused');
204
+ }
205
+ /** Export queue counts in Prometheus text exposition format. */
206
+ async exportPrometheusMetrics(globalVariables) {
207
+ this.assertOpen();
208
+ if (!this.queue.exportPrometheusMetrics) {
209
+ throw new Error('The installed BullMQ version does not support exportPrometheusMetrics().');
210
+ }
211
+ return this.queue.exportPrometheusMetrics(globalVariables);
212
+ }
213
+ /** Read BullMQ's per-minute completed/failed worker metrics from Redis. */
214
+ async getMetrics(type, start = 0, end = -1) {
215
+ this.assertOpen();
216
+ if (!this.queue.getMetrics) {
217
+ throw new Error('The installed BullMQ version does not support getMetrics().');
218
+ }
219
+ return this.queue.getMetrics(type, start, end);
220
+ }
221
+ /** Listen to lifecycle events across all workers using BullMQ QueueEvents. */
222
+ createQueueEvents() {
223
+ this.assertOpen();
224
+ if (!this.runtime.QueueEvents) {
225
+ throw new Error('The installed BullMQ runtime does not expose QueueEvents.');
226
+ }
227
+ const events = new this.runtime.QueueEvents(this.queueName, {
228
+ connection: this.config.connection,
229
+ ...(this.config.prefix ? { prefix: this.config.prefix } : {})
230
+ });
231
+ this.queueEvents.add(events);
232
+ return events;
233
+ }
234
+ /** Gracefully wait for active workers and release the BullMQ queue connection. */
235
+ async close() {
236
+ if (this.closed)
237
+ return;
238
+ this.closed = true;
239
+ const workerResults = await Promise.allSettled([
240
+ ...[...this.workers].map((worker) => worker.close()),
241
+ ...[...this.queueEvents].map((events) => events.close())
242
+ ]);
243
+ this.workers.clear();
244
+ this.queueEvents.clear();
245
+ const errors = workerResults.flatMap((result) => result.status === 'rejected' ? [result.reason] : []);
246
+ try {
247
+ await this.queue.close();
248
+ }
249
+ catch (error) {
250
+ errors.push(error);
251
+ }
252
+ if (errors.length > 0) {
253
+ throw new AggregateError(errors, 'One or more BullMQ resources failed to close cleanly.');
254
+ }
255
+ }
256
+ async serializeEmail(options) {
257
+ if (!options || typeof options !== 'object')
258
+ throw new Error('Email options are required');
259
+ const template = options.template;
260
+ const attachments = options.attachments;
261
+ const message = { ...options };
262
+ delete message.template;
263
+ delete message.templateData;
264
+ delete message.attachments;
265
+ let html = message.html;
266
+ if (html === undefined && template) {
267
+ html = await template.render(options);
268
+ }
269
+ const serializedAttachments = attachments?.map(serializeAttachment);
270
+ const email = {
271
+ ...message,
272
+ ...(html !== undefined ? { html } : {}),
273
+ ...(serializedAttachments ? { attachments: serializedAttachments } : {})
274
+ };
275
+ let payload;
276
+ try {
277
+ payload = JSON.stringify({ version: 1, email });
278
+ }
279
+ catch (error) {
280
+ throw new Error('Email job payload must be JSON serializable; template functions are rendered before enqueue.', { cause: error });
281
+ }
282
+ if (Buffer.byteLength(payload, 'utf8') > this.maxPayloadBytes) {
283
+ throw new Error(`Email job payload exceeds configured maxPayloadBytes (${this.maxPayloadBytes})`);
284
+ }
285
+ return JSON.parse(payload);
286
+ }
287
+ assertOpen() {
288
+ if (this.closed)
289
+ throw new Error('BullMQEmailQueue is closed. Create a new instance to resume queue operations.');
290
+ }
291
+ }
292
+ exports.BullMQEmailQueue = BullMQEmailQueue;
@@ -1,46 +1,24 @@
1
1
  import { IAttachment } from '../core/interfaces/email.interface';
2
+ export interface IAttachmentServiceOptions {
3
+ /** Restrict path attachments to this directory, resolved through realpath. */
4
+ allowedRoot?: string;
5
+ /** Optional maximum byte size for path- and buffer-backed attachments. */
6
+ maxSizeBytes?: number;
7
+ }
2
8
  /**
3
- * AttachmentService - Gerencia anexos para emails
4
- *
5
- * Métodos:
6
- * - addFromPath(filePath: string, options?: Partial<IAttachment>): Promise<IAttachment>
7
- * Adiciona um anexo a partir de um caminho de arquivo.
8
- *
9
- * - addFromBuffer(buffer: Buffer, filename: string, options?: Partial<IAttachment>): Promise<IAttachment>
10
- * Adiciona um anexo a partir de um buffer em memória.
11
- *
12
- * - addFromUrl(url: string, filename: string): Promise<IAttachment>
13
- * Adiciona um anexo a partir de uma URL (ainda não implementado).
14
- *
15
- * Exemplo de uso:
16
- *
17
- * const attachmentService = new AttachmentService();
18
- * const attachment = await attachmentService.addFromPath('path/to/file.pdf');
19
- *
20
- * emailFactory.sendEmail({
21
- * to: 'user@example.com',
22
- * subject: 'Assunto do Email',
23
- * template: myTemplate,
24
- * attachments: [{...attachment}]
25
- * });
9
+ * Creates Nodemailer attachment descriptors. Path attachments remain paths so
10
+ * Nodemailer can stream large files instead of loading them into memory.
11
+ * If allowedRoot is omitted, callers are responsible for supplying trusted paths.
26
12
  */
27
13
  export declare class AttachmentService {
28
- /**
29
- * addFromPath - Adiciona um anexo a partir de um caminho de arquivo
30
- * @param filePath - Caminho do arquivo a ser anexado
31
- * @param options - Opções adicionais para o anexo (filename, contentType, cid)
32
- * @returns Promise<IAttachment> - Objeto de anexo formatado para Nodemailer
33
- * @throws Error se o caminho não for um arquivo válido
34
- */
14
+ private readonly options;
15
+ private readonly maxSizeBytes?;
16
+ constructor(options?: IAttachmentServiceOptions);
17
+ /** Add an attachment as a file path, preserving streaming behavior. */
35
18
  addFromPath(filePath: string, options?: Partial<IAttachment>): Promise<IAttachment>;
36
19
  addFromBuffer(buffer: Buffer, filename: string, options?: Partial<IAttachment>): Promise<IAttachment>;
37
- /**
38
- * addFromUrl - Adiciona um anexo a partir de uma URL (a ser implementado)
39
- * @param url - URL do recurso a ser anexado
40
- * @param filename - Nome do arquivo para o anexo
41
- * @returns Promise<IAttachment> - Objeto de anexo formatado para Nodemailer
42
- * @throws Error se o método não for implementado
43
- */
20
+ /** URL attachments are deliberately not fetched by this service. */
44
21
  addFromUrl(url: string, filename: string): Promise<IAttachment>;
22
+ private assertSize;
45
23
  }
46
24
  //# sourceMappingURL=attachment.service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"attachment.service.d.ts","sourceRoot":"","sources":["../../src/services/attachment.service.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,iBAAiB;IAC5B;;;;;;OAMG;IACG,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC;IAcnF,aAAa,CACjB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC7B,OAAO,CAAC,WAAW,CAAC;IASvB;;;;;;OAMG;IACH,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;CAGhE"}
1
+ {"version":3,"file":"attachment.service.d.ts","sourceRoot":"","sources":["../../src/services/attachment.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAEjE,MAAM,WAAW,yBAAyB;IACxC,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,qBAAa,iBAAiB;IAGhB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAS;gBAEV,OAAO,GAAE,yBAA8B;IAQpE,uEAAuE;IACjE,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC;IAoCnF,aAAa,CACjB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC7B,OAAO,CAAC,WAAW,CAAC;IAiBvB,oEAAoE;IACpE,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAM/D,OAAO,CAAC,UAAU;CAKnB"}