@xenterprises/fastify-xemail 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.
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # @xenterprises/fastify-xemail
2
+
3
+ Fastify plugin for SendGrid email integration.
4
+
5
+ ## Features
6
+
7
+ - Send simple emails
8
+ - Send template-based emails
9
+ - Send emails with attachments
10
+ - Bulk sending (simple and personalized)
11
+ - Email validation
12
+ - Contact management (add, search, delete)
13
+ - List management
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @xenterprises/fastify-xemail
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```javascript
24
+ import Fastify from 'fastify';
25
+ import xEmail from '@xenterprises/fastify-xemail';
26
+
27
+ const fastify = Fastify();
28
+
29
+ await fastify.register(xEmail, {
30
+ apiKey: process.env.SENDGRID_API_KEY,
31
+ fromEmail: 'noreply@yourdomain.com'
32
+ });
33
+
34
+ // Usage example
35
+ fastify.get('/send-welcome', async (request, reply) => {
36
+ const { email, name } = request.query;
37
+
38
+ await fastify.email.send(
39
+ email,
40
+ 'Welcome!',
41
+ `<h1>Welcome ${name}!</h1>`,
42
+ `Welcome ${name}!`
43
+ );
44
+
45
+ return { success: true };
46
+ });
47
+ ```
48
+
49
+ ## API
50
+
51
+ ### `fastify.email.send(to, subject, html, text, extraOptions)`
52
+ Send a single email.
53
+
54
+ ### `fastify.email.sendTemplate(to, subject, templateId, dynamicData, extraOptions)`
55
+ Send an email using a SendGrid dynamic template.
56
+
57
+ ### `fastify.email.sendWithAttachments(to, subject, html, attachments)`
58
+ Send an email with attachments.
59
+
60
+ ### `fastify.email.validate(email)`
61
+ Validate an email address using SendGrid's Validation API.
62
+
63
+ ## License
64
+
65
+ ISC
package/index.d.ts ADDED
@@ -0,0 +1,325 @@
1
+ /**
2
+ * xEmail - Fastify Plugin for SendGrid Email
3
+ * TypeScript Type Definitions
4
+ *
5
+ * @module @xenterprises/fastify-xemail
6
+ * @version 1.0.0
7
+ */
8
+
9
+ import { FastifyPluginAsync } from 'fastify';
10
+
11
+ /**
12
+ * Email Options
13
+ */
14
+ export interface EmailOptions {
15
+ /** Email priority */
16
+ priority?: 'low' | 'normal' | 'high';
17
+
18
+ /** Custom headers */
19
+ headers?: Record<string, string>;
20
+
21
+ /** Reply-to address */
22
+ replyTo?: string;
23
+
24
+ /** BCC recipients */
25
+ bcc?: string[];
26
+
27
+ /** Custom categories for tracking */
28
+ categories?: string[];
29
+
30
+ /** Custom metadata */
31
+ metadata?: Record<string, string>;
32
+ }
33
+
34
+ /**
35
+ * Email with Attachments Options
36
+ */
37
+ export interface EmailWithAttachmentsOptions extends EmailOptions {
38
+ /** Attachments array */
39
+ attachments: Array<{
40
+ /** Attachment content (base64) */
41
+ content: string;
42
+
43
+ /** File name */
44
+ filename: string;
45
+
46
+ /** MIME type */
47
+ type: string;
48
+
49
+ /** Content disposition */
50
+ disposition?: 'inline' | 'attachment';
51
+
52
+ /** Content ID for inline attachments */
53
+ contentId?: string;
54
+ }>;
55
+ }
56
+
57
+ /**
58
+ * Email Send Result
59
+ */
60
+ export interface EmailSendResult {
61
+ /** SendGrid message ID */
62
+ messageId: string;
63
+
64
+ /** Send status */
65
+ status: 'success' | 'failed' | 'bounced' | 'spam' | 'blocked';
66
+
67
+ /** Recipient email */
68
+ to: string;
69
+
70
+ /** Subject line */
71
+ subject: string;
72
+
73
+ /** Timestamp of send */
74
+ timestamp: Date;
75
+
76
+ /** Error details if failed */
77
+ error?: {
78
+ code: number;
79
+ message: string;
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Email Contact
85
+ */
86
+ export interface EmailContact {
87
+ /** Contact ID */
88
+ id: string;
89
+
90
+ /** Email address */
91
+ email: string;
92
+
93
+ /** First name */
94
+ firstName?: string;
95
+
96
+ /** Last name */
97
+ lastName?: string;
98
+
99
+ /** Phone number */
100
+ phone?: string;
101
+
102
+ /** Company */
103
+ company?: string;
104
+
105
+ /** Custom fields */
106
+ customFields?: Record<string, string>;
107
+
108
+ /** Date added */
109
+ createdAt: Date;
110
+
111
+ /** Unsubscribed status */
112
+ isUnsubscribed?: boolean;
113
+ }
114
+
115
+ /**
116
+ * Email Contact List
117
+ */
118
+ export interface EmailContactList {
119
+ /** List ID */
120
+ id: string;
121
+
122
+ /** List name */
123
+ name: string;
124
+
125
+ /** Contact count */
126
+ contactCount: number;
127
+
128
+ /** Date created */
129
+ createdAt: Date;
130
+
131
+ /** Date modified */
132
+ modifiedAt: Date;
133
+ }
134
+
135
+ /**
136
+ * Email Service Methods
137
+ */
138
+ export interface EmailService {
139
+ /**
140
+ * Send email
141
+ * @param to Recipient email address
142
+ * @param subject Email subject
143
+ * @param html HTML content
144
+ * @param text Plain text content
145
+ * @param options Optional email options
146
+ * @returns Send result
147
+ */
148
+ send(
149
+ to: string,
150
+ subject: string,
151
+ html: string,
152
+ text?: string,
153
+ options?: EmailOptions
154
+ ): Promise<EmailSendResult>;
155
+
156
+ /**
157
+ * Send email from template
158
+ * @param to Recipient email address
159
+ * @param subject Email subject
160
+ * @param templateId SendGrid template ID
161
+ * @param dynamicData Template variable data
162
+ * @param options Optional email options
163
+ * @returns Send result
164
+ */
165
+ sendTemplate(
166
+ to: string,
167
+ subject: string,
168
+ templateId: string,
169
+ dynamicData: Record<string, any>,
170
+ options?: EmailOptions
171
+ ): Promise<EmailSendResult>;
172
+
173
+ /**
174
+ * Send email with attachments
175
+ * @param to Recipient email address
176
+ * @param subject Email subject
177
+ * @param html HTML content
178
+ * @param attachments Array of attachments
179
+ * @param options Optional email options
180
+ * @returns Send result
181
+ */
182
+ sendWithAttachments(
183
+ to: string,
184
+ subject: string,
185
+ html: string,
186
+ attachments: Array<{ content: string; filename: string; type: string }>,
187
+ options?: EmailOptions
188
+ ): Promise<EmailSendResult>;
189
+
190
+ /**
191
+ * Send bulk email to multiple recipients
192
+ * @param to Array of recipient emails
193
+ * @param subject Email subject
194
+ * @param html HTML content
195
+ * @param options Optional email options
196
+ * @returns Array of send results
197
+ */
198
+ sendBulk(
199
+ to: string[],
200
+ subject: string,
201
+ html: string,
202
+ options?: EmailOptions
203
+ ): Promise<EmailSendResult[]>;
204
+
205
+ /**
206
+ * Send personalized bulk email
207
+ * @param messages Array of personalized messages
208
+ * @returns Array of send results
209
+ */
210
+ sendPersonalizedBulk(
211
+ messages: Array<{
212
+ to: string;
213
+ subject: string;
214
+ html: string;
215
+ personalData?: Record<string, string>;
216
+ }>
217
+ ): Promise<EmailSendResult[]>;
218
+
219
+ /**
220
+ * Validate email address
221
+ * @param email Email to validate
222
+ * @returns Validation result
223
+ */
224
+ validate(
225
+ email: string
226
+ ): Promise<{ isValid: boolean; suggestion?: string; error?: string }>;
227
+
228
+ /**
229
+ * Add contact to SendGrid
230
+ * @param email Contact email
231
+ * @param data Contact data
232
+ * @param listIds Optional list IDs
233
+ * @returns Contact info
234
+ */
235
+ addContact(
236
+ email: string,
237
+ data: Record<string, string>,
238
+ listIds?: string[]
239
+ ): Promise<EmailContact>;
240
+
241
+ /**
242
+ * Search for contact
243
+ * @param email Contact email
244
+ * @returns Contact info or null
245
+ */
246
+ searchContact(email: string): Promise<EmailContact | null>;
247
+
248
+ /**
249
+ * Delete contact
250
+ * @param contactId Contact ID
251
+ * @returns Deletion result
252
+ */
253
+ deleteContact(contactId: string): Promise<{ success: boolean }>;
254
+
255
+ /**
256
+ * Create contact list
257
+ * @param name List name
258
+ * @returns List info
259
+ */
260
+ createList(name: string): Promise<EmailContactList>;
261
+
262
+ /**
263
+ * Get all contact lists
264
+ * @returns Array of lists
265
+ */
266
+ getLists(): Promise<EmailContactList[]>;
267
+
268
+ /**
269
+ * Delete contact list
270
+ * @param listId List ID
271
+ * @returns Deletion result
272
+ */
273
+ deleteList(listId: string): Promise<{ success: boolean }>;
274
+ }
275
+
276
+ /**
277
+ * Plugin Configuration Options
278
+ */
279
+ export interface XEmailPluginOptions {
280
+ /** SendGrid API Key */
281
+ apiKey: string;
282
+
283
+ /** From email address */
284
+ fromEmail: string;
285
+
286
+ /** From name */
287
+ fromName?: string;
288
+
289
+ /** Enable SendGrid service */
290
+ active?: boolean;
291
+ }
292
+
293
+ /**
294
+ * Fastify Instance with xEmail Decoration
295
+ */
296
+ declare module 'fastify' {
297
+ interface FastifyInstance {
298
+ /** Email service methods */
299
+ email: EmailService;
300
+ }
301
+ }
302
+
303
+ /**
304
+ * xEmail Plugin
305
+ * Registers SendGrid Email service with Fastify
306
+ *
307
+ * @example
308
+ * ```typescript
309
+ * import Fastify from 'fastify';
310
+ * import xEmail from '@xenterprises/fastify-xemail';
311
+ *
312
+ * const fastify = Fastify();
313
+ *
314
+ * await fastify.register(xEmail, {
315
+ * apiKey: process.env.SENDGRID_API_KEY,
316
+ * fromEmail: process.env.SENDGRID_FROM_EMAIL,
317
+ * });
318
+ *
319
+ * // Send email
320
+ * await fastify.email.send('user@example.com', 'Welcome', '<h1>Hello</h1>');
321
+ * ```
322
+ */
323
+ declare const xEmail: FastifyPluginAsync<XEmailPluginOptions>;
324
+
325
+ export default xEmail;
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@xenterprises/fastify-xemail",
3
+ "type": "module",
4
+ "version": "1.0.0",
5
+ "description": "Fastify plugin for SendGrid email integration.",
6
+ "main": "src/xEmail.js",
7
+ "exports": {
8
+ ".": "./src/xEmail.js"
9
+ },
10
+ "scripts": {
11
+ "test": "node --test test/xEmail.test.js"
12
+ },
13
+ "engines": {
14
+ "node": ">=20.0.0",
15
+ "npm": ">=10.0.0"
16
+ },
17
+ "keywords": [
18
+ "fastify",
19
+ "sendgrid",
20
+ "email",
21
+ "plugin"
22
+ ],
23
+ "author": "Tim Mushen",
24
+ "license": "ISC",
25
+ "devDependencies": {
26
+ "@types/node": "^22.7.4",
27
+ "fastify": "^5.1.0",
28
+ "fastify-plugin": "^5.0.0"
29
+ },
30
+ "dependencies": {
31
+ "@sendgrid/client": "^8.1.3",
32
+ "@sendgrid/mail": "^8.1.3",
33
+ "fastify-plugin": "^5.0.0"
34
+ },
35
+ "peerDependencies": {
36
+ "fastify": "^5.0.0"
37
+ }
38
+ }
package/src/xEmail.js ADDED
@@ -0,0 +1,368 @@
1
+ import fp from "fastify-plugin";
2
+ import sgMail from "@sendgrid/mail";
3
+ import sgClient from "@sendgrid/client";
4
+
5
+ async function xEmail(fastify, options) {
6
+ const { active = true, apiKey, fromEmail } = options;
7
+
8
+ if (active === false) return;
9
+
10
+ // Validate required credentials
11
+ if (!apiKey) {
12
+ throw new Error("SendGrid apiKey must be provided for Email service.");
13
+ }
14
+
15
+ if (!fromEmail) {
16
+ throw new Error("fromEmail must be provided for Email service.");
17
+ }
18
+
19
+ // Initialize SendGrid clients
20
+ sgMail.setApiKey(apiKey);
21
+ sgClient.setApiKey(apiKey);
22
+
23
+ console.info("\n 📧 Email Service (SendGrid) Initialized\n");
24
+
25
+ fastify.decorate("email", {
26
+ /**
27
+ * Send an email
28
+ * @param {string|string[]} to - Recipient email(s)
29
+ * @param {string} subject - Email subject
30
+ * @param {string} html - HTML content
31
+ * @param {string} text - Plain text content (optional)
32
+ * @param {object} extraOptions - Additional SendGrid options
33
+ * @returns {Promise<object>} Send result
34
+ */
35
+ send: async (to, subject, html, text = null, extraOptions = {}) => {
36
+ try {
37
+ const msg = {
38
+ to,
39
+ from: fromEmail,
40
+ subject,
41
+ html,
42
+ text: text || html.replace(/<[^>]*>/g, ""), // Strip HTML tags if no text provided
43
+ ...extraOptions,
44
+ };
45
+
46
+ const response = await sgMail.send(msg);
47
+ return { success: true, statusCode: response[0].statusCode, messageId: response[0].headers["x-message-id"] };
48
+ } catch (error) {
49
+ fastify.log.error("Email send failed:", error);
50
+ throw new Error("Failed to send email.");
51
+ }
52
+ },
53
+
54
+ /**
55
+ * Send an email using a template
56
+ * @param {string|string[]} to - Recipient email(s)
57
+ * @param {string} subject - Email subject
58
+ * @param {string} templateId - SendGrid template ID
59
+ * @param {object} dynamicData - Template variables
60
+ * @param {object} extraOptions - Additional SendGrid options
61
+ * @returns {Promise<object>} Send result
62
+ */
63
+ sendTemplate: async (to, subject, templateId, dynamicData = {}, extraOptions = {}) => {
64
+ try {
65
+ const msg = {
66
+ to,
67
+ from: fromEmail,
68
+ subject,
69
+ templateId,
70
+ dynamicTemplateData: { ...dynamicData, subject },
71
+ ...extraOptions,
72
+ };
73
+
74
+ const response = await sgMail.send(msg);
75
+ return { success: true, statusCode: response[0].statusCode, messageId: response[0].headers["x-message-id"] };
76
+ } catch (error) {
77
+ fastify.log.error("Email sendTemplate failed:", error);
78
+ throw new Error("Failed to send template email.");
79
+ }
80
+ },
81
+
82
+ /**
83
+ * Send an email with attachments
84
+ * @param {string|string[]} to - Recipient email(s)
85
+ * @param {string} subject - Email subject
86
+ * @param {string} html - HTML content
87
+ * @param {Array<{content: string, filename: string, type: string}>} attachments - Attachments
88
+ * @returns {Promise<object>} Send result
89
+ */
90
+ sendWithAttachments: async (to, subject, html, attachments) => {
91
+ try {
92
+ const msg = {
93
+ to,
94
+ from: fromEmail,
95
+ subject,
96
+ html,
97
+ attachments: attachments.map((att) => ({
98
+ content: att.content, // Base64 encoded
99
+ filename: att.filename,
100
+ type: att.type,
101
+ disposition: att.disposition || "attachment",
102
+ })),
103
+ };
104
+
105
+ const response = await sgMail.send(msg);
106
+ return { success: true, statusCode: response[0].statusCode };
107
+ } catch (error) {
108
+ fastify.log.error("Email sendWithAttachments failed:", error);
109
+ throw new Error("Failed to send email with attachments.");
110
+ }
111
+ },
112
+
113
+ /**
114
+ * Send bulk emails (multiple recipients, same content)
115
+ * @param {string[]} to - Array of recipient emails
116
+ * @param {string} subject - Email subject
117
+ * @param {string} html - HTML content
118
+ * @returns {Promise<object>} Bulk send result
119
+ */
120
+ sendBulk: async (to, subject, html) => {
121
+ try {
122
+ const msg = {
123
+ to,
124
+ from: fromEmail,
125
+ subject,
126
+ html,
127
+ };
128
+
129
+ const response = await sgMail.sendMultiple(msg);
130
+ return { success: true, count: to.length, statusCode: response[0].statusCode };
131
+ } catch (error) {
132
+ fastify.log.error("Email sendBulk failed:", error);
133
+ throw new Error("Failed to send bulk emails.");
134
+ }
135
+ },
136
+
137
+ /**
138
+ * Send personalized bulk emails (different content per recipient)
139
+ * @param {Array<{to: string, subject: string, html: string}>} messages - Array of message objects
140
+ * @returns {Promise<object[]>} Array of send results
141
+ */
142
+ sendPersonalizedBulk: async (messages) => {
143
+ try {
144
+ const mailMessages = messages.map((msg) => ({
145
+ to: msg.to,
146
+ from: fromEmail,
147
+ subject: msg.subject,
148
+ html: msg.html,
149
+ text: msg.text,
150
+ }));
151
+
152
+ const responses = await Promise.allSettled(mailMessages.map((msg) => sgMail.send(msg)));
153
+
154
+ return responses.map((result, index) => {
155
+ if (result.status === "fulfilled") {
156
+ return {
157
+ success: true,
158
+ to: messages[index].to,
159
+ statusCode: result.value[0].statusCode,
160
+ };
161
+ } else {
162
+ return {
163
+ success: false,
164
+ to: messages[index].to,
165
+ error: result.reason.message,
166
+ };
167
+ }
168
+ });
169
+ } catch (error) {
170
+ fastify.log.error("Email sendPersonalizedBulk failed:", error);
171
+ throw new Error("Failed to send personalized bulk emails.");
172
+ }
173
+ },
174
+
175
+ /**
176
+ * Validate an email address using SendGrid Validation API
177
+ * @param {string} email - Email to validate
178
+ * @returns {Promise<object>} Validation result
179
+ */
180
+ validate: async (email) => {
181
+ try {
182
+ const request = {
183
+ url: `/v3/validations/email`,
184
+ method: "POST",
185
+ body: { email },
186
+ };
187
+
188
+ const [response, body] = await sgClient.request(request);
189
+
190
+ if (response.statusCode === 200) {
191
+ return {
192
+ email,
193
+ valid: body.result?.verdict === "Valid",
194
+ verdict: body.result?.verdict,
195
+ score: body.result?.score,
196
+ result: body.result,
197
+ };
198
+ } else {
199
+ throw new Error(body.errors ? body.errors.map((err) => err.message).join(", ") : "Validation failed");
200
+ }
201
+ } catch (error) {
202
+ fastify.log.error("Email validation failed:", error);
203
+ return {
204
+ email,
205
+ valid: false,
206
+ verdict: "Unknown",
207
+ error: error.message,
208
+ };
209
+ }
210
+ },
211
+
212
+ /**
213
+ * Add or update a contact in SendGrid
214
+ * @param {string} email - Contact email
215
+ * @param {object} data - Contact data (first_name, last_name, custom fields)
216
+ * @param {string[]} listIds - Array of list IDs to add contact to
217
+ * @returns {Promise<object>} Contact result
218
+ */
219
+ addContact: async (email, data = {}, listIds = []) => {
220
+ try {
221
+ const request = {
222
+ url: `/v3/marketing/contacts`,
223
+ method: "PUT",
224
+ body: {
225
+ list_ids: listIds,
226
+ contacts: [
227
+ {
228
+ email,
229
+ first_name: data.firstName || data.first_name,
230
+ last_name: data.lastName || data.last_name,
231
+ ...data.customFields,
232
+ },
233
+ ],
234
+ },
235
+ };
236
+
237
+ const [response, body] = await sgClient.request(request);
238
+
239
+ if (response.statusCode === 200 || response.statusCode === 202) {
240
+ return {
241
+ success: true,
242
+ jobId: body.job_id,
243
+ email,
244
+ };
245
+ } else {
246
+ throw new Error("Failed to add contact");
247
+ }
248
+ } catch (error) {
249
+ fastify.log.error("Email addContact failed:", error);
250
+ throw new Error("Failed to add contact.");
251
+ }
252
+ },
253
+
254
+ /**
255
+ * Search for a contact by email
256
+ * @param {string} email - Contact email
257
+ * @returns {Promise<object>} Contact data
258
+ */
259
+ searchContact: async (email) => {
260
+ try {
261
+ const request = {
262
+ url: `/v3/marketing/contacts/search/emails`,
263
+ method: "POST",
264
+ body: { emails: [email] },
265
+ };
266
+
267
+ const [response, body] = await sgClient.request(request);
268
+
269
+ if (response.statusCode === 200 && body.result && body.result[email]) {
270
+ return {
271
+ found: true,
272
+ contact: body.result[email].contact,
273
+ };
274
+ } else {
275
+ return { found: false };
276
+ }
277
+ } catch (error) {
278
+ fastify.log.error("Email searchContact failed:", error);
279
+ throw new Error("Failed to search contact.");
280
+ }
281
+ },
282
+
283
+ /**
284
+ * Delete a contact by ID
285
+ * @param {string} contactId - Contact ID
286
+ * @returns {Promise<boolean>} Success status
287
+ */
288
+ deleteContact: async (contactId) => {
289
+ try {
290
+ const request = {
291
+ url: `/v3/marketing/contacts`,
292
+ method: "DELETE",
293
+ qs: { ids: contactId },
294
+ };
295
+
296
+ const [response] = await sgClient.request(request);
297
+ return response.statusCode === 202 || response.statusCode === 200;
298
+ } catch (error) {
299
+ fastify.log.error("Email deleteContact failed:", error);
300
+ throw new Error("Failed to delete contact.");
301
+ }
302
+ },
303
+
304
+ /**
305
+ * Create a new contact list
306
+ * @param {string} name - List name
307
+ * @returns {Promise<object>} List object
308
+ */
309
+ createList: async (name) => {
310
+ try {
311
+ const request = {
312
+ url: `/v3/marketing/lists`,
313
+ method: "POST",
314
+ body: { name },
315
+ };
316
+
317
+ const [response, body] = await sgClient.request(request);
318
+ return { success: true, list: body };
319
+ } catch (error) {
320
+ fastify.log.error("Email createList failed:", error);
321
+ throw new Error("Failed to create list.");
322
+ }
323
+ },
324
+
325
+ /**
326
+ * Get all contact lists
327
+ * @returns {Promise<object[]>} Array of lists
328
+ */
329
+ getLists: async () => {
330
+ try {
331
+ const request = {
332
+ url: `/v3/marketing/lists`,
333
+ method: "GET",
334
+ };
335
+
336
+ const [response, body] = await sgClient.request(request);
337
+ return body.result || [];
338
+ } catch (error) {
339
+ fastify.log.error("Email getLists failed:", error);
340
+ throw new Error("Failed to get lists.");
341
+ }
342
+ },
343
+
344
+ /**
345
+ * Delete a contact list
346
+ * @param {string} listId - List ID
347
+ * @returns {Promise<boolean>} Success status
348
+ */
349
+ deleteList: async (listId) => {
350
+ try {
351
+ const request = {
352
+ url: `/v3/marketing/lists/${listId}`,
353
+ method: "DELETE",
354
+ };
355
+
356
+ const [response] = await sgClient.request(request);
357
+ return response.statusCode === 202 || response.statusCode === 200 || response.statusCode === 204;
358
+ } catch (error) {
359
+ fastify.log.error("Email deleteList failed:", error);
360
+ throw new Error("Failed to delete list.");
361
+ }
362
+ },
363
+ });
364
+ }
365
+
366
+ export default fp(xEmail, {
367
+ name: "xEmail",
368
+ });