@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 +65 -0
- package/index.d.ts +325 -0
- package/package.json +38 -0
- package/src/xEmail.js +368 -0
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
|
+
});
|