mail-time 1.3.4 → 3.0.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.
@@ -0,0 +1,302 @@
1
+ import { logError } from '../helpers.js';
2
+
3
+ /** Class representing Redis Queue for MailTime */
4
+ class RedisQueue {
5
+ /**
6
+ * Create a RedisQueue instance
7
+ * @param {object} opts - configuration object
8
+ * @param {RedisClient} opts.client - Required, Redis'es `RedisClient` instance, like one returned from `await redis.createClient().connect()` method
9
+ * @param {string} [opts.prefix] - Optional prefix for scope isolation; use when creating multiple MailTime instances within the single application
10
+ */
11
+ constructor (opts) {
12
+ this.name = 'redis-queue';
13
+ if (!opts || typeof opts !== 'object' || opts === null) {
14
+ throw new TypeError('[mail-time] Configuration object must be passed into RedisQueue constructor');
15
+ }
16
+
17
+ if (!opts.client) {
18
+ throw new Error('[mail-time] [RedisQueue] required {client} option is missing, e.g. returned from `redis.createClient()` or `redis.createCluster()` method');
19
+ }
20
+
21
+ this.prefix = (typeof opts.prefix === 'string') ? opts.prefix : 'default';
22
+ this.uniqueName = `mailtime:${this.prefix}`;
23
+ this.client = opts.client;
24
+
25
+ // 3 types of keys are stored in Redis:
26
+ // 'letter' - JSON with email details
27
+ // 'sendat' - Timestamp used to scan/find/iterate over scheduled emails
28
+ // 'concatletter' — uuid of "pending" email for concatenation used when {concatEmails: true}
29
+
30
+ // Stored JSON structure:
31
+ // to {string|[string]}
32
+ // uuid {string}
33
+ // tries {number} - qty of send attempts
34
+ // sendAt {number} - When letter should be sent
35
+ // isSent {boolean} - Email status
36
+ // isCancelled {boolean} - `true` if email was cancelled before it was sent
37
+ // isFailed {boolean} - `true` if email has failed to send
38
+ // template {string} - Template for this email
39
+ // transport {number} - Last used transport
40
+ // concatSubject {string|boolean} - Email concatenation subject
41
+ // ---
42
+ // mailOptions {[object]} - Array of nodeMailer's `mailOptions`
43
+ // mailOptions.to {string|[string]} - [REQUIRED]
44
+ // mailOptions.from {string}
45
+ // mailOptions.text {string|boolean}
46
+ // mailOptions.html {string}
47
+ // mailOptions.subject {string}
48
+ // mailOptions.Other nodeMailer `sendMail` options...
49
+ }
50
+
51
+ /**
52
+ * @async
53
+ * @memberOf RedisQueue
54
+ * @name ping
55
+ * @description Check connection to Storage
56
+ * @returns {Promise<object>}
57
+ */
58
+ async ping() {
59
+ if (!this.mailTimeInstance) {
60
+ return {
61
+ status: 'Service Unavailable',
62
+ code: 503,
63
+ statusCode: 503,
64
+ error: new Error('MailTime instance not yet assigned to {mailTimeInstance} of Queue Adapter context'),
65
+ };
66
+ }
67
+
68
+ try {
69
+ const ping = await this.client.ping();
70
+ if (ping === 'PONG') {
71
+ return {
72
+ status: 'OK',
73
+ code: 200,
74
+ statusCode: 200,
75
+ };
76
+ }
77
+ } catch (pingError) {
78
+ return {
79
+ status: 'Internal Server Error',
80
+ code: 500,
81
+ statusCode: 500,
82
+ error: pingError
83
+ };
84
+ }
85
+
86
+ return {
87
+ status: 'Service Unavailable',
88
+ code: 503,
89
+ statusCode: 503,
90
+ error: new Error('Service Unavailable')
91
+ };
92
+ }
93
+
94
+ /**
95
+ * @memberOf RedisQueue
96
+ * @name iterate
97
+ * @description iterate over queued tasks passing to `mailTimeInstance.___send` method
98
+ * @returns {void 0}
99
+ */
100
+ async iterate() {
101
+ try {
102
+ const now = Date.now();
103
+ const cursor = this.client.scanIterator({
104
+ TYPE: 'string',
105
+ MATCH: this.__getKey('*', 'sendat'),
106
+ COUNT: 9999,
107
+ });
108
+
109
+ for await (const sendatKey of cursor) {
110
+ if (parseInt(await this.client.get(sendatKey)) <= now) {
111
+ await this.mailTimeInstance.___send(JSON.parse(await this.client.get(this.__getKey(sendatKey.split(':')[3]))));
112
+ }
113
+ }
114
+ } catch (iterateError) {
115
+ logError('[iterate] [for/await] [iterateError]', iterateError);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * @async
121
+ * @memberOf RedisQueue
122
+ * @name getPendingTo
123
+ * @description get queued task by `to` field (addressee)
124
+ * @param to {string} - email address
125
+ * @param sendAt {number} - timestamp
126
+ * @returns {Promise<object|null>}
127
+ */
128
+ async getPendingTo(to, sendAt) {
129
+ if (typeof to !== 'string' || typeof sendAt !== 'number') {
130
+ return null;
131
+ }
132
+
133
+ const concatKey = this.__getKey(to, 'concatletter');
134
+ let exists = await this.client.exists(concatKey);
135
+ if (!exists) {
136
+ return null;
137
+ }
138
+
139
+ const uuid = await this.client.get(concatKey);
140
+ const letterKey = this.__getKey(uuid, 'letter');
141
+ exists = await this.client.exists(letterKey);
142
+ if (!exists) {
143
+ return null;
144
+ }
145
+
146
+ const task = JSON.parse(await this.client.get(letterKey));
147
+ if (!task || task.isSent === true || task.isCancelled === true || task.isFailed === true) {
148
+ return null;
149
+ }
150
+
151
+ return task;
152
+ }
153
+
154
+ /**
155
+ * @async
156
+ * @memberOf RedisQueue
157
+ * @name push
158
+ * @description push task to the queue/storage
159
+ * @param task {object} - task's object
160
+ * @returns {Promise<void 0>}
161
+ */
162
+ async push(task) {
163
+ if (typeof task !== 'object') {
164
+ return;
165
+ }
166
+
167
+ if (task.sendAt instanceof Date) {
168
+ task.sendAt = +task.sendAt;
169
+ }
170
+
171
+ await this.client.set(this.__getKey(task.uuid, 'letter'), JSON.stringify(task));
172
+ await this.client.set(this.__getKey(task.uuid, 'sendat'), `${task.sendAt}`);
173
+ if (task.to) {
174
+ await this.client.set(this.__getKey(task.to, 'concatletter'), task.uuid, {
175
+ PXAT: task.sendAt - 128
176
+ });
177
+ }
178
+ }
179
+
180
+ /**
181
+ * @async
182
+ * @memberOf RedisQueue
183
+ * @name cancel
184
+ * @description cancel scheduled email
185
+ * @param uuid {string} - email's uuid
186
+ * @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found, was sent, or was cancelled previously
187
+ */
188
+ async cancel(uuid) {
189
+ if (typeof uuid !== 'string') {
190
+ return false;
191
+ }
192
+
193
+ await this.client.del(this.__getKey(uuid, 'sendat'));
194
+ const letterKey = this.__getKey(uuid, 'letter');
195
+ const exists = await this.client.exists(letterKey);
196
+ if (!exists) {
197
+ return false;
198
+ }
199
+
200
+ const task = JSON.parse(await this.client.get(letterKey));
201
+ if (!task || task.isSent === true || task.isCancelled === true) {
202
+ return false;
203
+ }
204
+
205
+ if (!this.mailTimeInstance.keepHistory) {
206
+ return await this.remove(task);
207
+ }
208
+
209
+ return await this.update(task, {
210
+ isCancelled: true,
211
+ });
212
+ }
213
+
214
+ /**
215
+ * @async
216
+ * @memberOf RedisQueue
217
+ * @name remove
218
+ * @description remove task from queue
219
+ * @param task {object} - task's object
220
+ * @returns {Promise<boolean>} returns `true` if removed or `false` if not found
221
+ */
222
+ async remove(task) {
223
+ if (typeof task !== 'object' || typeof task.uuid !== 'string') {
224
+ return false;
225
+ }
226
+
227
+ const letterKey = this.__getKey(task.uuid, 'letter');
228
+ const exists = await this.client.exists(letterKey);
229
+ if (!exists) {
230
+ return false;
231
+ }
232
+
233
+ await this.client.del([
234
+ letterKey,
235
+ this.__getKey(task.uuid, 'sendat'),
236
+ ]);
237
+ return true;
238
+ }
239
+
240
+ /**
241
+ * @async
242
+ * @memberOf RedisQueue
243
+ * @name update
244
+ * @description remove task from queue
245
+ * @param task {object} - task's object
246
+ * @param updateObj {object} - fields with new values to update
247
+ * @returns {Promise<boolean>} returns `true` if updated or `false` if not found or no changes was made
248
+ */
249
+ async update(task, updateObj) {
250
+ if (typeof task !== 'object' || typeof task.uuid !== 'string' || typeof updateObj !== 'object') {
251
+ return false;
252
+ }
253
+
254
+ const letterKey = this.__getKey(task.uuid, 'letter');
255
+
256
+ try {
257
+ const exists = await this.client.exists(letterKey);
258
+ if (!exists) {
259
+ return false;
260
+ }
261
+ const updatedTask = { ...task, ...updateObj };
262
+ await this.client.set(letterKey, JSON.stringify(updatedTask));
263
+
264
+ const sendatKey = this.__getKey(task.uuid, 'sendat');
265
+ if (updatedTask.isSent === true || updatedTask.isFailed === true || updatedTask.isCancelled === true) {
266
+ await this.client.del(sendatKey);
267
+ } else if (task.sendAt) {
268
+ await this.client.set(sendatKey, `${+task.sendAt}`);
269
+ }
270
+ return true;
271
+ } catch (opError) {
272
+ logError('[update] [try/catch] [opError]', opError);
273
+ return false;
274
+ }
275
+ }
276
+
277
+
278
+ /**
279
+ * @memberOf RedisQueue
280
+ * @name __getKey
281
+ * @description helper to generate scoped key
282
+ * @param uuid {string} - letter's uuid
283
+ * @param type {string} - "letter" or "sendat" or "concatletter"
284
+ * @returns {string} returns key used by Redis
285
+ */
286
+ __getKey(uuid, type = 'letter') {
287
+ if (!this.__keyTypes.includes(type)) {
288
+ throw new Error(`[mail-time] [RedisQueue] [__getKey] unsupported key "${type}" passed into the second argument`);
289
+ }
290
+ return `${this.uniqueName}:${type}:${uuid}`;
291
+ }
292
+
293
+ /**
294
+ * @memberOf RedisQueue
295
+ * @name __keyTypes
296
+ * @description list of supported key type
297
+ * @returns {string} returns key used by Redis
298
+ */
299
+ __keyTypes = ['letter', 'sendat', 'concatletter'];
300
+ }
301
+
302
+ export { RedisQueue };
package/helpers.js ADDED
@@ -0,0 +1,11 @@
1
+ const debug = (isDebug, ...args) => {
2
+ if (isDebug) {
3
+ console.info.call(console, '[DEBUG] [mail-time]', `${new Date}`, ...args);
4
+ }
5
+ };
6
+
7
+ const logError = (...args) => {
8
+ console.error.call(console, '[ERROR] [mail-time]', `${new Date}`, ...args);
9
+ };
10
+
11
+ export { debug, logError };