mail-time 1.4.0 → 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.
- package/README.md +468 -118
- package/adapters/blank-example.js +230 -0
- package/adapters/mongo.js +310 -0
- package/adapters/redis.js +302 -0
- package/helpers.js +11 -0
- package/index.cjs +968 -341
- package/index.js +407 -392
- package/package.json +8 -7
|
@@ -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 };
|