@xenterprises/fastify-xtwilio 1.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,362 @@
1
+ // src/services/email.js
2
+ import sgMail from "@sendgrid/mail";
3
+ import sgClient from "@sendgrid/client";
4
+
5
+ export async function setupEmail(fastify, options) {
6
+ if (options.active === false) return;
7
+
8
+ // Validate required credentials
9
+ if (!options.apiKey) {
10
+ throw new Error("SendGrid apiKey must be provided for Email service.");
11
+ }
12
+
13
+ if (!options.fromEmail) {
14
+ throw new Error("fromEmail must be provided for Email service.");
15
+ }
16
+
17
+ // Initialize SendGrid clients
18
+ sgMail.setApiKey(options.apiKey);
19
+ sgClient.setApiKey(options.apiKey);
20
+
21
+ fastify.decorate("email", {
22
+ /**
23
+ * Send an email
24
+ * @param {string|string[]} to - Recipient email(s)
25
+ * @param {string} subject - Email subject
26
+ * @param {string} html - HTML content
27
+ * @param {string} text - Plain text content (optional)
28
+ * @param {object} extraOptions - Additional SendGrid options
29
+ * @returns {Promise<object>} Send result
30
+ */
31
+ send: async (to, subject, html, text = null, extraOptions = {}) => {
32
+ try {
33
+ const msg = {
34
+ to,
35
+ from: options.fromEmail,
36
+ subject,
37
+ html,
38
+ text: text || html.replace(/<[^>]*>/g, ""), // Strip HTML tags if no text provided
39
+ ...extraOptions,
40
+ };
41
+
42
+ const response = await sgMail.send(msg);
43
+ return { success: true, statusCode: response[0].statusCode, messageId: response[0].headers["x-message-id"] };
44
+ } catch (error) {
45
+ fastify.log.error("Email send failed:", error);
46
+ throw new Error("Failed to send email.");
47
+ }
48
+ },
49
+
50
+ /**
51
+ * Send an email using a template
52
+ * @param {string|string[]} to - Recipient email(s)
53
+ * @param {string} subject - Email subject
54
+ * @param {string} templateId - SendGrid template ID
55
+ * @param {object} dynamicData - Template variables
56
+ * @param {object} extraOptions - Additional SendGrid options
57
+ * @returns {Promise<object>} Send result
58
+ */
59
+ sendTemplate: async (to, subject, templateId, dynamicData = {}, extraOptions = {}) => {
60
+ try {
61
+ const msg = {
62
+ to,
63
+ from: options.fromEmail,
64
+ subject,
65
+ templateId,
66
+ dynamicTemplateData: { ...dynamicData, subject },
67
+ ...extraOptions,
68
+ };
69
+
70
+ const response = await sgMail.send(msg);
71
+ return { success: true, statusCode: response[0].statusCode, messageId: response[0].headers["x-message-id"] };
72
+ } catch (error) {
73
+ fastify.log.error("Email sendTemplate failed:", error);
74
+ throw new Error("Failed to send template email.");
75
+ }
76
+ },
77
+
78
+ /**
79
+ * Send an email with attachments
80
+ * @param {string|string[]} to - Recipient email(s)
81
+ * @param {string} subject - Email subject
82
+ * @param {string} html - HTML content
83
+ * @param {Array<{content: string, filename: string, type: string}>} attachments - Attachments
84
+ * @returns {Promise<object>} Send result
85
+ */
86
+ sendWithAttachments: async (to, subject, html, attachments) => {
87
+ try {
88
+ const msg = {
89
+ to,
90
+ from: options.fromEmail,
91
+ subject,
92
+ html,
93
+ attachments: attachments.map((att) => ({
94
+ content: att.content, // Base64 encoded
95
+ filename: att.filename,
96
+ type: att.type,
97
+ disposition: att.disposition || "attachment",
98
+ })),
99
+ };
100
+
101
+ const response = await sgMail.send(msg);
102
+ return { success: true, statusCode: response[0].statusCode };
103
+ } catch (error) {
104
+ fastify.log.error("Email sendWithAttachments failed:", error);
105
+ throw new Error("Failed to send email with attachments.");
106
+ }
107
+ },
108
+
109
+ /**
110
+ * Send bulk emails (multiple recipients, same content)
111
+ * @param {string[]} to - Array of recipient emails
112
+ * @param {string} subject - Email subject
113
+ * @param {string} html - HTML content
114
+ * @returns {Promise<object>} Bulk send result
115
+ */
116
+ sendBulk: async (to, subject, html) => {
117
+ try {
118
+ const msg = {
119
+ to,
120
+ from: options.fromEmail,
121
+ subject,
122
+ html,
123
+ };
124
+
125
+ const response = await sgMail.sendMultiple(msg);
126
+ return { success: true, count: to.length, statusCode: response[0].statusCode };
127
+ } catch (error) {
128
+ fastify.log.error("Email sendBulk failed:", error);
129
+ throw new Error("Failed to send bulk emails.");
130
+ }
131
+ },
132
+
133
+ /**
134
+ * Send personalized bulk emails (different content per recipient)
135
+ * @param {Array<{to: string, subject: string, html: string}>} messages - Array of message objects
136
+ * @returns {Promise<object[]>} Array of send results
137
+ */
138
+ sendPersonalizedBulk: async (messages) => {
139
+ try {
140
+ const mailMessages = messages.map((msg) => ({
141
+ to: msg.to,
142
+ from: options.fromEmail,
143
+ subject: msg.subject,
144
+ html: msg.html,
145
+ text: msg.text,
146
+ }));
147
+
148
+ const responses = await Promise.allSettled(mailMessages.map((msg) => sgMail.send(msg)));
149
+
150
+ return responses.map((result, index) => {
151
+ if (result.status === "fulfilled") {
152
+ return {
153
+ success: true,
154
+ to: messages[index].to,
155
+ statusCode: result.value[0].statusCode,
156
+ };
157
+ } else {
158
+ return {
159
+ success: false,
160
+ to: messages[index].to,
161
+ error: result.reason.message,
162
+ };
163
+ }
164
+ });
165
+ } catch (error) {
166
+ fastify.log.error("Email sendPersonalizedBulk failed:", error);
167
+ throw new Error("Failed to send personalized bulk emails.");
168
+ }
169
+ },
170
+
171
+ /**
172
+ * Validate an email address using SendGrid Validation API
173
+ * @param {string} email - Email to validate
174
+ * @returns {Promise<object>} Validation result
175
+ */
176
+ validate: async (email) => {
177
+ try {
178
+ const request = {
179
+ url: `/v3/validations/email`,
180
+ method: "POST",
181
+ body: { email },
182
+ };
183
+
184
+ const [response, body] = await sgClient.request(request);
185
+
186
+ if (response.statusCode === 200) {
187
+ return {
188
+ email,
189
+ valid: body.result?.verdict === "Valid",
190
+ verdict: body.result?.verdict,
191
+ score: body.result?.score,
192
+ result: body.result,
193
+ };
194
+ } else {
195
+ throw new Error(body.errors ? body.errors.map((err) => err.message).join(", ") : "Validation failed");
196
+ }
197
+ } catch (error) {
198
+ fastify.log.error("Email validation failed:", error);
199
+ return {
200
+ email,
201
+ valid: false,
202
+ verdict: "Unknown",
203
+ error: error.message,
204
+ };
205
+ }
206
+ },
207
+
208
+ /**
209
+ * Add or update a contact in SendGrid
210
+ * @param {string} email - Contact email
211
+ * @param {object} data - Contact data (first_name, last_name, custom fields)
212
+ * @param {string[]} listIds - Array of list IDs to add contact to
213
+ * @returns {Promise<object>} Contact result
214
+ */
215
+ addContact: async (email, data = {}, listIds = []) => {
216
+ try {
217
+ const request = {
218
+ url: `/v3/marketing/contacts`,
219
+ method: "PUT",
220
+ body: {
221
+ list_ids: listIds,
222
+ contacts: [
223
+ {
224
+ email,
225
+ first_name: data.firstName || data.first_name,
226
+ last_name: data.lastName || data.last_name,
227
+ ...data.customFields,
228
+ },
229
+ ],
230
+ },
231
+ };
232
+
233
+ const [response, body] = await sgClient.request(request);
234
+
235
+ if (response.statusCode === 200 || response.statusCode === 202) {
236
+ return {
237
+ success: true,
238
+ jobId: body.job_id,
239
+ email,
240
+ };
241
+ } else {
242
+ throw new Error("Failed to add contact");
243
+ }
244
+ } catch (error) {
245
+ fastify.log.error("Email addContact failed:", error);
246
+ throw new Error("Failed to add contact.");
247
+ }
248
+ },
249
+
250
+ /**
251
+ * Search for a contact by email
252
+ * @param {string} email - Contact email
253
+ * @returns {Promise<object>} Contact data
254
+ */
255
+ searchContact: async (email) => {
256
+ try {
257
+ const request = {
258
+ url: `/v3/marketing/contacts/search/emails`,
259
+ method: "POST",
260
+ body: { emails: [email] },
261
+ };
262
+
263
+ const [response, body] = await sgClient.request(request);
264
+
265
+ if (response.statusCode === 200 && body.result && body.result[email]) {
266
+ return {
267
+ found: true,
268
+ contact: body.result[email].contact,
269
+ };
270
+ } else {
271
+ return { found: false };
272
+ }
273
+ } catch (error) {
274
+ fastify.log.error("Email searchContact failed:", error);
275
+ throw new Error("Failed to search contact.");
276
+ }
277
+ },
278
+
279
+ /**
280
+ * Delete a contact by ID
281
+ * @param {string} contactId - Contact ID
282
+ * @returns {Promise<boolean>} Success status
283
+ */
284
+ deleteContact: async (contactId) => {
285
+ try {
286
+ const request = {
287
+ url: `/v3/marketing/contacts`,
288
+ method: "DELETE",
289
+ qs: { ids: contactId },
290
+ };
291
+
292
+ const [response] = await sgClient.request(request);
293
+ return response.statusCode === 202 || response.statusCode === 200;
294
+ } catch (error) {
295
+ fastify.log.error("Email deleteContact failed:", error);
296
+ throw new Error("Failed to delete contact.");
297
+ }
298
+ },
299
+
300
+ /**
301
+ * Create a new contact list
302
+ * @param {string} name - List name
303
+ * @returns {Promise<object>} List object
304
+ */
305
+ createList: async (name) => {
306
+ try {
307
+ const request = {
308
+ url: `/v3/marketing/lists`,
309
+ method: "POST",
310
+ body: { name },
311
+ };
312
+
313
+ const [response, body] = await sgClient.request(request);
314
+ return { success: true, list: body };
315
+ } catch (error) {
316
+ fastify.log.error("Email createList failed:", error);
317
+ throw new Error("Failed to create list.");
318
+ }
319
+ },
320
+
321
+ /**
322
+ * Get all contact lists
323
+ * @returns {Promise<object[]>} Array of lists
324
+ */
325
+ getLists: async () => {
326
+ try {
327
+ const request = {
328
+ url: `/v3/marketing/lists`,
329
+ method: "GET",
330
+ };
331
+
332
+ const [response, body] = await sgClient.request(request);
333
+ return body.result || [];
334
+ } catch (error) {
335
+ fastify.log.error("Email getLists failed:", error);
336
+ throw new Error("Failed to get lists.");
337
+ }
338
+ },
339
+
340
+ /**
341
+ * Delete a contact list
342
+ * @param {string} listId - List ID
343
+ * @returns {Promise<boolean>} Success status
344
+ */
345
+ deleteList: async (listId) => {
346
+ try {
347
+ const request = {
348
+ url: `/v3/marketing/lists/${listId}`,
349
+ method: "DELETE",
350
+ };
351
+
352
+ const [response] = await sgClient.request(request);
353
+ return response.statusCode === 202 || response.statusCode === 200 || response.statusCode === 204;
354
+ } catch (error) {
355
+ fastify.log.error("Email deleteList failed:", error);
356
+ throw new Error("Failed to delete list.");
357
+ }
358
+ },
359
+ });
360
+
361
+ console.info(" ✅ Email Service (SendGrid) Enabled");
362
+ }
@@ -0,0 +1,284 @@
1
+ // src/services/rcs.js
2
+ import Twilio from "twilio";
3
+
4
+ export async function setupRCS(fastify, options) {
5
+ if (options.active === false) return;
6
+
7
+ // Validate required credentials
8
+ if (!options.accountSid || !options.authToken) {
9
+ throw new Error("Twilio accountSid and authToken must be provided for RCS.");
10
+ }
11
+
12
+ if (!options.messagingServiceSid) {
13
+ throw new Error("messagingServiceSid is required for RCS messaging.");
14
+ }
15
+
16
+ // Initialize Twilio client
17
+ const twilioClient = Twilio(options.accountSid, options.authToken);
18
+
19
+ fastify.decorate("rcs", {
20
+ /**
21
+ * Send a basic RCS text message
22
+ * @param {string} to - Recipient phone number (E.164 format)
23
+ * @param {string} body - Message content
24
+ * @param {object} extraOptions - Optional parameters
25
+ * @returns {Promise<object>} Message result
26
+ */
27
+ send: async (to, body, extraOptions = {}) => {
28
+ try {
29
+ const message = await twilioClient.messages.create({
30
+ body,
31
+ to,
32
+ messagingServiceSid: options.messagingServiceSid,
33
+ contentRetention: "retain",
34
+ ...extraOptions,
35
+ });
36
+ return message;
37
+ } catch (error) {
38
+ fastify.log.error("RCS send failed:", error);
39
+ throw new Error("Failed to send RCS message.");
40
+ }
41
+ },
42
+
43
+ /**
44
+ * Send an RCS message with media
45
+ * @param {string} to - Recipient phone number
46
+ * @param {string} body - Message content
47
+ * @param {string|string[]} mediaUrl - Media URL(s)
48
+ * @returns {Promise<object>} Message result
49
+ */
50
+ sendMedia: async (to, body, mediaUrl) => {
51
+ try {
52
+ const message = await twilioClient.messages.create({
53
+ body,
54
+ to,
55
+ messagingServiceSid: options.messagingServiceSid,
56
+ mediaUrl: Array.isArray(mediaUrl) ? mediaUrl : [mediaUrl],
57
+ });
58
+ return message;
59
+ } catch (error) {
60
+ fastify.log.error("RCS sendMedia failed:", error);
61
+ throw new Error("Failed to send RCS media message.");
62
+ }
63
+ },
64
+
65
+ /**
66
+ * Send an RCS message using Content Template
67
+ * @param {string} to - Recipient phone number
68
+ * @param {string} contentSid - Twilio Content Template SID
69
+ * @param {object} contentVariables - Template variables
70
+ * @returns {Promise<object>} Message result
71
+ */
72
+ sendTemplate: async (to, contentSid, contentVariables = {}) => {
73
+ try {
74
+ const message = await twilioClient.messages.create({
75
+ to,
76
+ messagingServiceSid: options.messagingServiceSid,
77
+ contentSid,
78
+ contentVariables: JSON.stringify(contentVariables),
79
+ });
80
+ return message;
81
+ } catch (error) {
82
+ fastify.log.error("RCS sendTemplate failed:", error);
83
+ throw new Error("Failed to send RCS template message.");
84
+ }
85
+ },
86
+
87
+ /**
88
+ * Send an RCS rich card message
89
+ * @param {string} to - Recipient phone number
90
+ * @param {object} card - Rich card configuration
91
+ * @param {string} card.title - Card title
92
+ * @param {string} card.description - Card description
93
+ * @param {string} card.mediaUrl - Card media URL
94
+ * @param {Array} card.actions - Card actions/buttons
95
+ * @returns {Promise<object>} Message result
96
+ */
97
+ sendRichCard: async (to, card) => {
98
+ try {
99
+ // Build content for rich card using Twilio Content API
100
+ const contentData = {
101
+ types: {
102
+ "twilio/card": {
103
+ title: card.title,
104
+ subtitle: card.description,
105
+ media: card.mediaUrl ? [card.mediaUrl] : [],
106
+ actions: card.actions || [],
107
+ },
108
+ },
109
+ };
110
+
111
+ // Create content template if contentSid not provided
112
+ let contentSid = card.contentSid;
113
+ if (!contentSid) {
114
+ const content = await twilioClient.content.v1.contents.create({
115
+ friendly_name: `RCS Card ${Date.now()}`,
116
+ types: contentData.types,
117
+ });
118
+ contentSid = content.sid;
119
+ }
120
+
121
+ const message = await twilioClient.messages.create({
122
+ to,
123
+ messagingServiceSid: options.messagingServiceSid,
124
+ contentSid,
125
+ });
126
+
127
+ return message;
128
+ } catch (error) {
129
+ fastify.log.error("RCS sendRichCard failed:", error);
130
+ throw new Error("Failed to send RCS rich card.");
131
+ }
132
+ },
133
+
134
+ /**
135
+ * Send an RCS carousel message
136
+ * @param {string} to - Recipient phone number
137
+ * @param {object[]} cards - Array of card objects
138
+ * @returns {Promise<object>} Message result
139
+ */
140
+ sendCarousel: async (to, cards) => {
141
+ try {
142
+ const contentData = {
143
+ types: {
144
+ "twilio/list-picker": {
145
+ body: "Please select an option",
146
+ items: cards.map((card) => ({
147
+ id: card.id || `card_${Date.now()}`,
148
+ title: card.title,
149
+ description: card.description,
150
+ media: card.mediaUrl,
151
+ })),
152
+ },
153
+ },
154
+ };
155
+
156
+ const content = await twilioClient.content.v1.contents.create({
157
+ friendly_name: `RCS Carousel ${Date.now()}`,
158
+ types: contentData.types,
159
+ });
160
+
161
+ const message = await twilioClient.messages.create({
162
+ to,
163
+ messagingServiceSid: options.messagingServiceSid,
164
+ contentSid: content.sid,
165
+ });
166
+
167
+ return message;
168
+ } catch (error) {
169
+ fastify.log.error("RCS sendCarousel failed:", error);
170
+ throw new Error("Failed to send RCS carousel.");
171
+ }
172
+ },
173
+
174
+ /**
175
+ * Send an RCS message with suggested quick replies
176
+ * @param {string} to - Recipient phone number
177
+ * @param {string} body - Message content
178
+ * @param {Array<{text: string, payload: string}>} replies - Quick reply options
179
+ * @returns {Promise<object>} Message result
180
+ */
181
+ sendQuickReplies: async (to, body, replies) => {
182
+ try {
183
+ const contentData = {
184
+ types: {
185
+ "twilio/quick-reply": {
186
+ body,
187
+ actions: replies.map((reply) => ({
188
+ type: "quick-reply",
189
+ title: reply.text,
190
+ id: reply.payload || reply.text,
191
+ })),
192
+ },
193
+ },
194
+ };
195
+
196
+ const content = await twilioClient.content.v1.contents.create({
197
+ friendly_name: `RCS Quick Reply ${Date.now()}`,
198
+ types: contentData.types,
199
+ });
200
+
201
+ const message = await twilioClient.messages.create({
202
+ to,
203
+ messagingServiceSid: options.messagingServiceSid,
204
+ contentSid: content.sid,
205
+ });
206
+
207
+ return message;
208
+ } catch (error) {
209
+ fastify.log.error("RCS sendQuickReplies failed:", error);
210
+ throw new Error("Failed to send RCS quick replies.");
211
+ }
212
+ },
213
+
214
+ /**
215
+ * Get RCS message status
216
+ * @param {string} messageSid - Message SID
217
+ * @returns {Promise<object>} Message status
218
+ */
219
+ getStatus: async (messageSid) => {
220
+ try {
221
+ const message = await twilioClient.messages(messageSid).fetch();
222
+ return {
223
+ sid: message.sid,
224
+ status: message.status,
225
+ errorCode: message.errorCode,
226
+ errorMessage: message.errorMessage,
227
+ channel: message.channel,
228
+ };
229
+ } catch (error) {
230
+ fastify.log.error("RCS getStatus failed:", error);
231
+ throw new Error("Failed to get RCS message status.");
232
+ }
233
+ },
234
+
235
+ /**
236
+ * List RCS content templates
237
+ * @param {object} options - List options
238
+ * @returns {Promise<object[]>} List of content templates
239
+ */
240
+ listTemplates: async (listOptions = {}) => {
241
+ try {
242
+ const contents = await twilioClient.content.v1.contents.list({
243
+ limit: listOptions.limit || 50,
244
+ });
245
+ return contents;
246
+ } catch (error) {
247
+ fastify.log.error("RCS listTemplates failed:", error);
248
+ throw new Error("Failed to list RCS templates.");
249
+ }
250
+ },
251
+
252
+ /**
253
+ * Get a content template by SID
254
+ * @param {string} contentSid - Content template SID
255
+ * @returns {Promise<object>} Content template
256
+ */
257
+ getTemplate: async (contentSid) => {
258
+ try {
259
+ const content = await twilioClient.content.v1.contents(contentSid).fetch();
260
+ return content;
261
+ } catch (error) {
262
+ fastify.log.error("RCS getTemplate failed:", error);
263
+ throw new Error("Failed to get RCS template.");
264
+ }
265
+ },
266
+
267
+ /**
268
+ * Delete a content template
269
+ * @param {string} contentSid - Content template SID
270
+ * @returns {Promise<boolean>} Success status
271
+ */
272
+ deleteTemplate: async (contentSid) => {
273
+ try {
274
+ await twilioClient.content.v1.contents(contentSid).remove();
275
+ return true;
276
+ } catch (error) {
277
+ fastify.log.error("RCS deleteTemplate failed:", error);
278
+ throw new Error("Failed to delete RCS template.");
279
+ }
280
+ },
281
+ });
282
+
283
+ console.info(" ✅ RCS Service Enabled");
284
+ }