@xenterprises/fastify-xemail 1.0.2 → 1.1.1

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.
Files changed (5) hide show
  1. package/LICENSE +100 -0
  2. package/README.md +167 -35
  3. package/index.d.ts +86 -278
  4. package/package.json +7 -5
  5. package/src/xEmail.js +188 -151
package/LICENSE ADDED
@@ -0,0 +1,100 @@
1
+ PROPRIETARY SOFTWARE LICENSE
2
+
3
+ Copyright (c) 2024-2026 X Enterprises LLC. All Rights Reserved.
4
+
5
+ This software and associated documentation files (the "Software") are the
6
+ exclusive property of X Enterprises LLC, a Washington limited liability
7
+ company ("X Enterprises"). The Software is distributed through public
8
+ package registries (including npm) for operational convenience only; such
9
+ distribution does not grant any rights beyond those expressly stated below.
10
+
11
+ TERMS AND CONDITIONS
12
+
13
+ 1. OWNERSHIP
14
+ All rights, title, and interest in and to the Software, including all
15
+ intellectual property rights, are and shall remain the exclusive property
16
+ of X Enterprises. No rights are granted except as expressly set forth in
17
+ this License.
18
+
19
+ 2. PERMITTED USE
20
+ Subject to the restrictions in Section 3, you are permitted to download,
21
+ install, and execute the Software solely as a dependency of:
22
+
23
+ (a) software developed, owned, or operated by X Enterprises;
24
+
25
+ (b) software that X Enterprises has developed, delivered, or licensed to
26
+ a third party ("Client") under a written engagement agreement with
27
+ X Enterprises, when such use is performed by or on behalf of that
28
+ Client; or
29
+
30
+ (c) end-user access to, or consumption of, a product or service described
31
+ in (a) or (b), provided that such access does not involve
32
+ redistribution, modification, or separate use of the Software.
33
+
34
+ Permitted Use includes automated installation and execution by continuous
35
+ integration systems, container builds, hosting platforms, and similar
36
+ infrastructure, to the extent necessary to support (a), (b), or (c).
37
+
38
+ 3. RESTRICTIONS
39
+ Except as expressly permitted in Section 2, and without the prior written
40
+ consent of X Enterprises, you may not:
41
+
42
+ (a) copy, modify, adapt, translate, or create derivative works of the
43
+ Software for any purpose outside the scope of Section 2;
44
+
45
+ (b) redistribute, republish, sublicense, sell, lease, rent, or otherwise
46
+ transfer the Software, in whole or in part, whether standalone or
47
+ bundled with other software;
48
+
49
+ (c) reverse engineer, decompile, disassemble, or attempt to derive the
50
+ source code or underlying ideas, algorithms, structure, or
51
+ organization of the Software, except to the extent such activity is
52
+ expressly permitted by applicable law notwithstanding this
53
+ restriction;
54
+
55
+ (d) use the Software, in whole or in part, to develop, operate, or
56
+ provide any product or service that competes with or substitutes for
57
+ any X Enterprises product or service;
58
+
59
+ (e) remove, obscure, or alter any copyright, trademark, license, or other
60
+ proprietary notice contained in or on the Software; or
61
+
62
+ (f) use the Software in violation of any applicable law or regulation.
63
+
64
+ 4. NO WARRANTY
65
+ THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
66
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
67
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL
68
+ X ENTERPRISES BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY,
69
+ WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT
70
+ OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
71
+ THE SOFTWARE.
72
+
73
+ 5. LIMITATION OF LIABILITY
74
+ IN NO EVENT SHALL X ENTERPRISES BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
75
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
76
+ TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
77
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
78
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
79
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
80
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
81
+
82
+ 6. GOVERNING LAW
83
+ This License shall be governed by and construed in accordance with the
84
+ laws of the State of Washington, United States, without regard to its
85
+ conflict of law provisions. Exclusive jurisdiction for any dispute
86
+ arising out of this License shall lie in the state or federal courts
87
+ located in King County, Washington.
88
+
89
+ 7. TERMINATION
90
+ This License is effective until terminated. Your rights under this
91
+ License will terminate automatically and without notice if you fail to
92
+ comply with any term herein. Upon termination, you must cease all use of
93
+ the Software and destroy all copies in your possession or control.
94
+ Sections 1, 3, 4, 5, 6, and 7 survive termination.
95
+
96
+ For licensing inquiries, contact: legal@x.enterprises
97
+
98
+ ---
99
+ X Enterprises LLC
100
+ Bothell, Washington, United States
package/README.md CHANGED
@@ -1,16 +1,6 @@
1
1
  # @xenterprises/fastify-xemail
2
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
3
+ Fastify plugin for SendGrid email — send transactional emails, template emails, bulk messages, validate addresses, and manage marketing contacts and lists.
14
4
 
15
5
  ## Installation
16
6
 
@@ -28,38 +18,180 @@ const fastify = Fastify();
28
18
 
29
19
  await fastify.register(xEmail, {
30
20
  apiKey: process.env.SENDGRID_API_KEY,
31
- fromEmail: 'noreply@yourdomain.com'
21
+ fromEmail: process.env.SENDGRID_FROM_EMAIL,
22
+ fromName: 'My App', // optional
32
23
  });
33
24
 
34
- // Usage example
35
- fastify.get('/send-welcome', async (request, reply) => {
36
- const { email, name } = request.query;
37
-
38
- await fastify.xEmail.send(
39
- email,
40
- 'Welcome!',
41
- `<h1>Welcome ${name}!</h1>`,
42
- `Welcome ${name}!`
43
- );
44
-
45
- return { success: true };
46
- });
25
+ // Send a simple email
26
+ await fastify.xEmail.send(
27
+ 'user@example.com',
28
+ 'Welcome!',
29
+ '<h1>Welcome!</h1><p>Thanks for signing up.</p>'
30
+ );
31
+ ```
32
+
33
+ ## Options
34
+
35
+ | Name | Type | Default | Required | Description |
36
+ |------|------|---------|----------|-------------|
37
+ | `apiKey` | `string` | — | Yes | SendGrid API key |
38
+ | `fromEmail` | `string` | — | Yes | Verified sender email address |
39
+ | `fromName` | `string` | — | No | Sender display name |
40
+ | `active` | `boolean` | `true` | No | Set `false` to disable the plugin entirely |
41
+
42
+ ## Decorated Methods
43
+
44
+ All methods are available on `fastify.xEmail`.
45
+
46
+ ### `send(to, subject, html, text?, extraOptions?)`
47
+
48
+ Send an email. Plain text is auto-generated from HTML if omitted.
49
+
50
+ ```javascript
51
+ const result = await fastify.xEmail.send('user@example.com', 'Hello', '<p>Hi</p>');
52
+ // { success: true, statusCode: 202, messageId: 'xxx' }
53
+ ```
54
+
55
+ ### `sendTemplate(to, subject, templateId, dynamicData?, extraOptions?)`
56
+
57
+ Send using a SendGrid dynamic template.
58
+
59
+ ```javascript
60
+ await fastify.xEmail.sendTemplate(
61
+ 'user@example.com',
62
+ 'Welcome',
63
+ 'd-abc123def456',
64
+ { firstName: 'Tim', actionUrl: 'https://example.com/verify' }
65
+ );
66
+ ```
67
+
68
+ ### `sendWithAttachments(to, subject, html, attachments)`
69
+
70
+ Send an email with file attachments. Each attachment needs `content` (base64), `filename`, and `type` (MIME).
71
+
72
+ ```javascript
73
+ await fastify.xEmail.sendWithAttachments(
74
+ 'user@example.com',
75
+ 'Your Invoice',
76
+ '<p>See attached.</p>',
77
+ [{ content: base64String, filename: 'invoice.pdf', type: 'application/pdf' }]
78
+ );
79
+ ```
80
+
81
+ ### `sendBulk(to, subject, html)`
82
+
83
+ Send the same email to multiple recipients at once.
84
+
85
+ ```javascript
86
+ await fastify.xEmail.sendBulk(
87
+ ['a@example.com', 'b@example.com'],
88
+ 'Announcement',
89
+ '<p>Big news!</p>'
90
+ );
91
+ // { success: true, count: 2, statusCode: 202 }
92
+ ```
93
+
94
+ ### `sendPersonalizedBulk(messages)`
95
+
96
+ Send different content to each recipient. Returns per-recipient results including partial failures.
97
+
98
+ ```javascript
99
+ const results = await fastify.xEmail.sendPersonalizedBulk([
100
+ { to: 'a@example.com', subject: 'Hi A', html: '<p>Hello A</p>' },
101
+ { to: 'b@example.com', subject: 'Hi B', html: '<p>Hello B</p>' },
102
+ ]);
103
+ // [{ success: true, to: 'a@example.com', statusCode: 202 }, ...]
47
104
  ```
48
105
 
49
- ## API
106
+ ### `validate(email)`
107
+
108
+ Validate an email address using the SendGrid Email Validation API. Returns a soft result on API errors instead of throwing.
109
+
110
+ ```javascript
111
+ const result = await fastify.xEmail.validate('user@example.com');
112
+ // { email: '...', valid: true, verdict: 'Valid', score: 0.95, result: {...} }
113
+ ```
114
+
115
+ ### `addContact(email, data?, listIds?)`
116
+
117
+ Add or update a contact in SendGrid Marketing. Accepts both `firstName`/`lastName` and `first_name`/`last_name`.
118
+
119
+ ```javascript
120
+ await fastify.xEmail.addContact('user@example.com', {
121
+ firstName: 'Tim',
122
+ lastName: 'Smith',
123
+ customFields: { w1_T: 'premium' }
124
+ }, ['list-id-1']);
125
+ // { success: true, jobId: '...', email: '...' }
126
+ ```
127
+
128
+ ### `searchContact(email)`
129
+
130
+ Search for a contact by email.
131
+
132
+ ```javascript
133
+ const result = await fastify.xEmail.searchContact('user@example.com');
134
+ // { found: true, contact: { id: '...', email: '...', ... } }
135
+ ```
136
+
137
+ ### `deleteContact(contactId)`
138
+
139
+ Delete a contact by ID. Returns `true` on success.
140
+
141
+ ### `createList(name)`
142
+
143
+ Create a new marketing contact list.
144
+
145
+ ```javascript
146
+ const result = await fastify.xEmail.createList('Newsletter');
147
+ // { success: true, list: { id: '...', name: 'Newsletter' } }
148
+ ```
149
+
150
+ ### `getLists()`
151
+
152
+ Get all contact lists. Returns an array.
153
+
154
+ ### `deleteList(listId)`
155
+
156
+ Delete a contact list by ID. Returns `true` on success.
157
+
158
+ ## Environment Variables
159
+
160
+ | Name | Required | Description |
161
+ |------|----------|-------------|
162
+ | `SENDGRID_API_KEY` | Yes | SendGrid API key with Mail Send permissions |
163
+ | `SENDGRID_FROM_EMAIL` | Yes | Verified sender email address |
164
+ | `SENDGRID_FROM_NAME` | No | Sender display name |
165
+
166
+ ## Error Reference
167
+
168
+ All errors are prefixed with `[xEmail]` for easy identification in logs.
169
+
170
+ | Error | When |
171
+ |-------|------|
172
+ | `[xEmail] 'apiKey' (string) is required.` | Missing or non-string `apiKey` at registration |
173
+ | `[xEmail] 'fromEmail' (string) is required.` | Missing or non-string `fromEmail` at registration |
174
+ | `[xEmail] 'to' is required for send().` | Calling `send()` without a recipient |
175
+ | `[xEmail] 'subject' is required for send().` | Calling `send()` without a subject |
176
+ | `[xEmail] 'html' is required for send().` | Calling `send()` without HTML content |
177
+ | `[xEmail] 'templateId' is required for sendTemplate().` | Missing template ID |
178
+ | `[xEmail] 'attachments' must be a non-empty array.` | Empty or non-array attachments |
179
+ | `[xEmail] 'to' must be a non-empty array for sendBulk().` | Non-array or empty array for bulk |
180
+ | `[xEmail] 'messages' must be a non-empty array for sendPersonalizedBulk().` | Empty messages array |
181
+ | `[xEmail] 'email' (string) is required for validate().` | Missing email for validation |
182
+ | `[xEmail] Failed to send email: <reason>` | SendGrid API rejection on send |
183
+ | `[xEmail] Failed to add contact: <reason>` | SendGrid API rejection on contact add |
184
+
185
+ ## How It Works
50
186
 
51
- ### `fastify.xEmail.send(to, subject, html, text, extraOptions)`
52
- Send a single email.
187
+ The plugin initializes both `@sendgrid/mail` (for transactional emails) and `@sendgrid/client` (for the Marketing and Validation APIs) with the provided API key. It decorates the Fastify instance with `fastify.xEmail`, an object containing all email and contact management methods.
53
188
 
54
- ### `fastify.xEmail.sendTemplate(to, subject, templateId, dynamicData, extraOptions)`
55
- Send an email using a SendGrid dynamic template.
189
+ Each method validates its required parameters before making any API call. Transactional methods (`send`, `sendTemplate`, `sendWithAttachments`, `sendBulk`) use `sgMail`, while marketing and validation methods (`validate`, `addContact`, `searchContact`, `deleteContact`, `createList`, `getLists`, `deleteList`) use `sgClient` with direct REST calls.
56
190
 
57
- ### `fastify.xEmail.sendWithAttachments(to, subject, html, attachments)`
58
- Send an email with attachments.
191
+ Errors from SendGrid are caught, logged via `fastify.log.error` (with structured error objects, never credentials), and re-thrown with a descriptive `[xEmail]` prefix. The `validate()` method is an exception — it returns a soft error result instead of throwing, since validation failures are expected in normal operation.
59
192
 
60
- ### `fastify.xEmail.validate(email)`
61
- Validate an email address using SendGrid's Validation API.
193
+ Setting `active: false` causes the plugin to return immediately without decorating, which is useful for disabling email in test environments.
62
194
 
63
195
  ## License
64
196
 
65
- ISC
197
+ UNLICENSED
package/index.d.ts CHANGED
@@ -1,325 +1,133 @@
1
1
  /**
2
2
  * xEmail - Fastify Plugin for SendGrid Email
3
- * TypeScript Type Definitions
4
- *
5
3
  * @module @xenterprises/fastify-xemail
6
- * @version 1.0.0
7
4
  */
8
5
 
9
6
  import { FastifyPluginAsync } from 'fastify';
10
7
 
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 */
8
+ /** Additional SendGrid mail options passed through to the API. */
9
+ export interface EmailExtraOptions {
22
10
  replyTo?: string;
23
-
24
- /** BCC recipients */
25
- bcc?: string[];
26
-
27
- /** Custom categories for tracking */
11
+ bcc?: string | string[];
12
+ cc?: string | string[];
28
13
  categories?: string[];
29
-
30
- /** Custom metadata */
31
- metadata?: Record<string, string>;
14
+ headers?: Record<string, string>;
15
+ [key: string]: unknown;
32
16
  }
33
17
 
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
- }>;
18
+ /** Attachment object for sendWithAttachments(). */
19
+ export interface EmailAttachment {
20
+ /** Base64-encoded file content */
21
+ content: string;
22
+ /** File name */
23
+ filename: string;
24
+ /** MIME type (e.g. 'application/pdf') */
25
+ type: string;
26
+ /** Content disposition: 'attachment' (default) or 'inline' */
27
+ disposition?: 'inline' | 'attachment';
55
28
  }
56
29
 
57
- /**
58
- * Email Send Result
59
- */
30
+ /** Result from send(), sendTemplate(), sendWithAttachments(). */
60
31
  export interface EmailSendResult {
61
- /** SendGrid message ID */
32
+ success: boolean;
33
+ statusCode: number;
62
34
  messageId: string;
35
+ }
63
36
 
64
- /** Send status */
65
- status: 'success' | 'failed' | 'bounced' | 'spam' | 'blocked';
37
+ /** Result from sendBulk(). */
38
+ export interface EmailBulkResult {
39
+ success: boolean;
40
+ count: number;
41
+ statusCode: number;
42
+ }
66
43
 
67
- /** Recipient email */
44
+ /** Per-recipient result from sendPersonalizedBulk(). */
45
+ export interface EmailPersonalizedResult {
46
+ success: boolean;
68
47
  to: string;
48
+ statusCode?: number;
49
+ error?: string;
50
+ }
69
51
 
70
- /** Subject line */
71
- subject: string;
72
-
73
- /** Timestamp of send */
74
- timestamp: Date;
52
+ /** Result from validate(). */
53
+ export interface EmailValidationResult {
54
+ email: string;
55
+ valid: boolean;
56
+ verdict: string;
57
+ score?: number;
58
+ result?: Record<string, unknown>;
59
+ error?: string;
60
+ }
75
61
 
76
- /** Error details if failed */
77
- error?: {
78
- code: number;
79
- message: string;
80
- };
62
+ /** Result from addContact(). */
63
+ export interface EmailAddContactResult {
64
+ success: boolean;
65
+ jobId: string;
66
+ email: string;
81
67
  }
82
68
 
83
- /**
84
- * Email Contact
85
- */
86
- export interface EmailContact {
87
- /** Contact ID */
88
- id: string;
69
+ /** Result from searchContact(). */
70
+ export interface EmailSearchContactResult {
71
+ found: boolean;
72
+ contact?: Record<string, unknown>;
73
+ }
89
74
 
90
- /** Email address */
91
- email: string;
75
+ /** Result from createList(). */
76
+ export interface EmailCreateListResult {
77
+ success: boolean;
78
+ list: Record<string, unknown>;
79
+ }
92
80
 
93
- /** First name */
81
+ /** Contact data for addContact(). */
82
+ export interface ContactData {
94
83
  firstName?: string;
95
-
96
- /** Last name */
84
+ first_name?: string;
97
85
  lastName?: string;
98
-
99
- /** Phone number */
100
- phone?: string;
101
-
102
- /** Company */
103
- company?: string;
104
-
105
- /** Custom fields */
86
+ last_name?: string;
106
87
  customFields?: Record<string, string>;
107
-
108
- /** Date added */
109
- createdAt: Date;
110
-
111
- /** Unsubscribed status */
112
- isUnsubscribed?: boolean;
113
88
  }
114
89
 
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;
90
+ /** Message object for sendPersonalizedBulk(). */
91
+ export interface PersonalizedMessage {
92
+ to: string;
93
+ subject: string;
94
+ html: string;
95
+ text?: string;
133
96
  }
134
97
 
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 }>;
98
+ /** All methods decorated onto fastify.xEmail. */
99
+ export interface XEmailService {
100
+ send(to: string | string[], subject: string, html: string, text?: string | null, extraOptions?: EmailExtraOptions): Promise<EmailSendResult>;
101
+ sendTemplate(to: string | string[], subject: string, templateId: string, dynamicData?: Record<string, unknown>, extraOptions?: EmailExtraOptions): Promise<EmailSendResult>;
102
+ sendWithAttachments(to: string | string[], subject: string, html: string, attachments: EmailAttachment[]): Promise<EmailSendResult>;
103
+ sendBulk(to: string[], subject: string, html: string): Promise<EmailBulkResult>;
104
+ sendPersonalizedBulk(messages: PersonalizedMessage[]): Promise<EmailPersonalizedResult[]>;
105
+ validate(email: string): Promise<EmailValidationResult>;
106
+ addContact(email: string, data?: ContactData, listIds?: string[]): Promise<EmailAddContactResult>;
107
+ searchContact(email: string): Promise<EmailSearchContactResult>;
108
+ deleteContact(contactId: string): Promise<boolean>;
109
+ createList(name: string): Promise<EmailCreateListResult>;
110
+ getLists(): Promise<Record<string, unknown>[]>;
111
+ deleteList(listId: string): Promise<boolean>;
274
112
  }
275
113
 
276
- /**
277
- * Plugin Configuration Options
278
- */
114
+ /** Plugin configuration options. */
279
115
  export interface XEmailPluginOptions {
280
- /** SendGrid API Key */
116
+ /** SendGrid API key (required) */
281
117
  apiKey: string;
282
-
283
- /** From email address */
118
+ /** Verified sender email address (required) */
284
119
  fromEmail: string;
285
-
286
- /** From name */
120
+ /** Sender display name (optional) */
287
121
  fromName?: string;
288
-
289
- /** Enable SendGrid service */
122
+ /** Set to false to disable the plugin (default: true) */
290
123
  active?: boolean;
291
124
  }
292
125
 
293
- /**
294
- * Fastify Instance with xEmail Decoration
295
- */
296
126
  declare module 'fastify' {
297
127
  interface FastifyInstance {
298
- /** Email service methods */
299
- xEmail: EmailService;
128
+ xEmail: XEmailService;
300
129
  }
301
130
  }
302
131
 
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.xEmail.send('user@example.com', 'Welcome', '<h1>Hello</h1>');
321
- * ```
322
- */
323
132
  declare const xEmail: FastifyPluginAsync<XEmailPluginOptions>;
324
-
325
133
  export default xEmail;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@xenterprises/fastify-xemail",
3
3
  "type": "module",
4
- "version": "1.0.2",
5
- "description": "Fastify plugin for SendGrid email integration.",
4
+ "version": "1.1.1",
5
+ "description": "Fastify plugin for SendGrid email integration — transactional emails, templates, bulk sending, validation, and contact management.",
6
6
  "main": "src/xEmail.js",
7
7
  "exports": {
8
8
  ".": "./src/xEmail.js"
@@ -21,11 +21,10 @@
21
21
  "plugin"
22
22
  ],
23
23
  "author": "Tim Mushen",
24
- "license": "ISC",
24
+ "license": "SEE LICENSE IN LICENSE",
25
25
  "devDependencies": {
26
26
  "@types/node": "^22.7.4",
27
- "fastify": "^5.1.0",
28
- "fastify-plugin": "^5.0.0"
27
+ "fastify": "^5.1.0"
29
28
  },
30
29
  "dependencies": {
31
30
  "@sendgrid/client": "^8.1.3",
@@ -34,5 +33,8 @@
34
33
  },
35
34
  "peerDependencies": {
36
35
  "fastify": "^5.0.0"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
37
39
  }
38
40
  }
package/src/xEmail.js CHANGED
@@ -2,69 +2,89 @@ import fp from "fastify-plugin";
2
2
  import sgMail from "@sendgrid/mail";
3
3
  import sgClient from "@sendgrid/client";
4
4
 
5
+ /**
6
+ * @param {import('fastify').FastifyInstance} fastify
7
+ * @param {object} options
8
+ * @param {string} options.apiKey - SendGrid API key
9
+ * @param {string} options.fromEmail - Verified sender email address
10
+ * @param {string} [options.fromName] - Sender display name
11
+ * @param {boolean} [options.active=true] - Enable/disable the plugin
12
+ */
5
13
  async function xEmail(fastify, options) {
6
- const { active = true, apiKey, fromEmail } = options;
14
+ const { active = true, apiKey, fromEmail, fromName } = options;
7
15
 
8
16
  if (active === false) return;
9
17
 
10
- // Validate required credentials
11
- if (!apiKey) {
12
- throw new Error("SendGrid apiKey must be provided for Email service.");
18
+ if (!apiKey || typeof apiKey !== "string") {
19
+ throw new Error("[xEmail] 'apiKey' (string) is required.");
13
20
  }
14
21
 
15
- if (!fromEmail) {
16
- throw new Error("fromEmail must be provided for Email service.");
22
+ if (!fromEmail || typeof fromEmail !== "string") {
23
+ throw new Error("[xEmail] 'fromEmail' (string) is required.");
17
24
  }
18
25
 
19
- // Initialize SendGrid clients
20
26
  sgMail.setApiKey(apiKey);
21
27
  sgClient.setApiKey(apiKey);
22
28
 
23
- console.info("\n 📧 Email Service (SendGrid) Initialized\n");
29
+ const from = fromName ? { email: fromEmail, name: fromName } : fromEmail;
30
+
31
+ fastify.log.info("xEmail (SendGrid) initialized");
24
32
 
25
33
  fastify.decorate("xEmail", {
26
34
  /**
27
- * Send an email
35
+ * Send an email.
28
36
  * @param {string|string[]} to - Recipient email(s)
29
37
  * @param {string} subject - Email subject
30
38
  * @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
39
+ * @param {string} [text] - Plain text fallback (auto-generated from HTML if omitted)
40
+ * @param {object} [extraOptions] - Additional SendGrid mail options
41
+ * @returns {Promise<{success: boolean, statusCode: number, messageId: string}>}
34
42
  */
35
43
  send: async (to, subject, html, text = null, extraOptions = {}) => {
44
+ if (!to) throw new Error("[xEmail] 'to' is required for send().");
45
+ if (!subject) throw new Error("[xEmail] 'subject' is required for send().");
46
+ if (!html) throw new Error("[xEmail] 'html' is required for send().");
47
+
36
48
  try {
37
49
  const msg = {
38
50
  to,
39
- from: fromEmail,
51
+ from,
40
52
  subject,
41
53
  html,
42
- text: text || html.replace(/<[^>]*>/g, ""), // Strip HTML tags if no text provided
54
+ text: text || html.replace(/<[^>]*>/g, ""),
43
55
  ...extraOptions,
44
56
  };
45
57
 
46
58
  const response = await sgMail.send(msg);
47
- return { success: true, statusCode: response[0].statusCode, messageId: response[0].headers["x-message-id"] };
59
+ return {
60
+ success: true,
61
+ statusCode: response[0].statusCode,
62
+ messageId: response[0].headers["x-message-id"],
63
+ };
48
64
  } catch (error) {
49
- fastify.log.error("Email send failed:", error);
50
- throw new Error("Failed to send email.");
65
+ fastify.log.error({ err: error }, "xEmail send failed");
66
+ throw new Error(`[xEmail] Failed to send email: ${error.message}`);
51
67
  }
52
68
  },
53
69
 
54
70
  /**
55
- * Send an email using a template
71
+ * Send an email using a SendGrid dynamic template.
56
72
  * @param {string|string[]} to - Recipient email(s)
57
73
  * @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
74
+ * @param {string} templateId - SendGrid dynamic template ID (d-xxx)
75
+ * @param {object} [dynamicData] - Template variables
76
+ * @param {object} [extraOptions] - Additional SendGrid mail options
77
+ * @returns {Promise<{success: boolean, statusCode: number, messageId: string}>}
62
78
  */
63
79
  sendTemplate: async (to, subject, templateId, dynamicData = {}, extraOptions = {}) => {
80
+ if (!to) throw new Error("[xEmail] 'to' is required for sendTemplate().");
81
+ if (!subject) throw new Error("[xEmail] 'subject' is required for sendTemplate().");
82
+ if (!templateId) throw new Error("[xEmail] 'templateId' is required for sendTemplate().");
83
+
64
84
  try {
65
85
  const msg = {
66
86
  to,
67
- from: fromEmail,
87
+ from,
68
88
  subject,
69
89
  templateId,
70
90
  dynamicTemplateData: { ...dynamicData, subject },
@@ -72,30 +92,41 @@ async function xEmail(fastify, options) {
72
92
  };
73
93
 
74
94
  const response = await sgMail.send(msg);
75
- return { success: true, statusCode: response[0].statusCode, messageId: response[0].headers["x-message-id"] };
95
+ return {
96
+ success: true,
97
+ statusCode: response[0].statusCode,
98
+ messageId: response[0].headers["x-message-id"],
99
+ };
76
100
  } catch (error) {
77
- fastify.log.error("Email sendTemplate failed:", error);
78
- throw new Error("Failed to send template email.");
101
+ fastify.log.error({ err: error }, "xEmail sendTemplate failed");
102
+ throw new Error(`[xEmail] Failed to send template email: ${error.message}`);
79
103
  }
80
104
  },
81
105
 
82
106
  /**
83
- * Send an email with attachments
107
+ * Send an email with file attachments.
84
108
  * @param {string|string[]} to - Recipient email(s)
85
109
  * @param {string} subject - Email subject
86
110
  * @param {string} html - HTML content
87
- * @param {Array<{content: string, filename: string, type: string}>} attachments - Attachments
88
- * @returns {Promise<object>} Send result
111
+ * @param {Array<{content: string, filename: string, type: string, disposition?: string}>} attachments
112
+ * @returns {Promise<{success: boolean, statusCode: number, messageId: string}>}
89
113
  */
90
114
  sendWithAttachments: async (to, subject, html, attachments) => {
115
+ if (!to) throw new Error("[xEmail] 'to' is required for sendWithAttachments().");
116
+ if (!subject) throw new Error("[xEmail] 'subject' is required for sendWithAttachments().");
117
+ if (!html) throw new Error("[xEmail] 'html' is required for sendWithAttachments().");
118
+ if (!Array.isArray(attachments) || attachments.length === 0) {
119
+ throw new Error("[xEmail] 'attachments' must be a non-empty array.");
120
+ }
121
+
91
122
  try {
92
123
  const msg = {
93
124
  to,
94
- from: fromEmail,
125
+ from,
95
126
  subject,
96
127
  html,
97
128
  attachments: attachments.map((att) => ({
98
- content: att.content, // Base64 encoded
129
+ content: att.content,
99
130
  filename: att.filename,
100
131
  type: att.type,
101
132
  disposition: att.disposition || "attachment",
@@ -103,89 +134,93 @@ async function xEmail(fastify, options) {
103
134
  };
104
135
 
105
136
  const response = await sgMail.send(msg);
106
- return { success: true, statusCode: response[0].statusCode };
137
+ return {
138
+ success: true,
139
+ statusCode: response[0].statusCode,
140
+ messageId: response[0].headers["x-message-id"],
141
+ };
107
142
  } catch (error) {
108
- fastify.log.error("Email sendWithAttachments failed:", error);
109
- throw new Error("Failed to send email with attachments.");
143
+ fastify.log.error({ err: error }, "xEmail sendWithAttachments failed");
144
+ throw new Error(`[xEmail] Failed to send email with attachments: ${error.message}`);
110
145
  }
111
146
  },
112
147
 
113
148
  /**
114
- * Send bulk emails (multiple recipients, same content)
149
+ * Send bulk emails (same content to multiple recipients).
115
150
  * @param {string[]} to - Array of recipient emails
116
151
  * @param {string} subject - Email subject
117
152
  * @param {string} html - HTML content
118
- * @returns {Promise<object>} Bulk send result
153
+ * @returns {Promise<{success: boolean, count: number, statusCode: number}>}
119
154
  */
120
155
  sendBulk: async (to, subject, html) => {
121
- try {
122
- const msg = {
123
- to,
124
- from: fromEmail,
125
- subject,
126
- html,
127
- };
156
+ if (!Array.isArray(to) || to.length === 0) {
157
+ throw new Error("[xEmail] 'to' must be a non-empty array for sendBulk().");
158
+ }
159
+ if (!subject) throw new Error("[xEmail] 'subject' is required for sendBulk().");
160
+ if (!html) throw new Error("[xEmail] 'html' is required for sendBulk().");
128
161
 
162
+ try {
163
+ const msg = { to, from, subject, html };
129
164
  const response = await sgMail.sendMultiple(msg);
130
165
  return { success: true, count: to.length, statusCode: response[0].statusCode };
131
166
  } catch (error) {
132
- fastify.log.error("Email sendBulk failed:", error);
133
- throw new Error("Failed to send bulk emails.");
167
+ fastify.log.error({ err: error }, "xEmail sendBulk failed");
168
+ throw new Error(`[xEmail] Failed to send bulk emails: ${error.message}`);
134
169
  }
135
170
  },
136
171
 
137
172
  /**
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
173
+ * Send personalized emails (different content per recipient).
174
+ * @param {Array<{to: string, subject: string, html: string, text?: string}>} messages
175
+ * @returns {Promise<Array<{success: boolean, to: string, statusCode?: number, error?: string}>>}
141
176
  */
142
177
  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.");
178
+ if (!Array.isArray(messages) || messages.length === 0) {
179
+ throw new Error("[xEmail] 'messages' must be a non-empty array for sendPersonalizedBulk().");
172
180
  }
181
+
182
+ const mailMessages = messages.map((msg) => ({
183
+ to: msg.to,
184
+ from,
185
+ subject: msg.subject,
186
+ html: msg.html,
187
+ text: msg.text,
188
+ }));
189
+
190
+ const responses = await Promise.allSettled(mailMessages.map((msg) => sgMail.send(msg)));
191
+
192
+ return responses.map((result, index) => {
193
+ if (result.status === "fulfilled") {
194
+ return {
195
+ success: true,
196
+ to: messages[index].to,
197
+ statusCode: result.value[0].statusCode,
198
+ };
199
+ }
200
+ return {
201
+ success: false,
202
+ to: messages[index].to,
203
+ error: result.reason.message,
204
+ };
205
+ });
173
206
  },
174
207
 
175
208
  /**
176
- * Validate an email address using SendGrid Validation API
209
+ * Validate an email address using SendGrid Email Validation API.
177
210
  * @param {string} email - Email to validate
178
- * @returns {Promise<object>} Validation result
211
+ * @returns {Promise<{email: string, valid: boolean, verdict: string, score?: number, result?: object, error?: string}>}
179
212
  */
180
213
  validate: async (email) => {
214
+ if (!email || typeof email !== "string") {
215
+ throw new Error("[xEmail] 'email' (string) is required for validate().");
216
+ }
217
+
181
218
  try {
182
- const request = {
219
+ const [response, body] = await sgClient.request({
183
220
  url: `/v3/validations/email`,
184
221
  method: "POST",
185
222
  body: { email },
186
- };
187
-
188
- const [response, body] = await sgClient.request(request);
223
+ });
189
224
 
190
225
  if (response.statusCode === 200) {
191
226
  return {
@@ -195,11 +230,11 @@ async function xEmail(fastify, options) {
195
230
  score: body.result?.score,
196
231
  result: body.result,
197
232
  };
198
- } else {
199
- throw new Error(body.errors ? body.errors.map((err) => err.message).join(", ") : "Validation failed");
200
233
  }
234
+
235
+ throw new Error(body.errors ? body.errors.map((e) => e.message).join(", ") : "Validation failed");
201
236
  } catch (error) {
202
- fastify.log.error("Email validation failed:", error);
237
+ fastify.log.error({ err: error }, "xEmail validate failed");
203
238
  return {
204
239
  email,
205
240
  valid: false,
@@ -210,15 +245,19 @@ async function xEmail(fastify, options) {
210
245
  },
211
246
 
212
247
  /**
213
- * Add or update a contact in SendGrid
248
+ * Add or update a contact in SendGrid Marketing.
214
249
  * @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
250
+ * @param {object} [data] - Contact data (firstName, lastName, customFields)
251
+ * @param {string[]} [listIds] - List IDs to add the contact to
252
+ * @returns {Promise<{success: boolean, jobId: string, email: string}>}
218
253
  */
219
254
  addContact: async (email, data = {}, listIds = []) => {
255
+ if (!email || typeof email !== "string") {
256
+ throw new Error("[xEmail] 'email' (string) is required for addContact().");
257
+ }
258
+
220
259
  try {
221
- const request = {
260
+ const [response, body] = await sgClient.request({
222
261
  url: `/v3/marketing/contacts`,
223
262
  method: "PUT",
224
263
  body: {
@@ -232,132 +271,129 @@ async function xEmail(fastify, options) {
232
271
  },
233
272
  ],
234
273
  },
235
- };
236
-
237
- const [response, body] = await sgClient.request(request);
274
+ });
238
275
 
239
276
  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");
277
+ return { success: true, jobId: body.job_id, email };
247
278
  }
279
+
280
+ throw new Error("Unexpected status code: " + response.statusCode);
248
281
  } catch (error) {
249
- fastify.log.error("Email addContact failed:", error);
250
- throw new Error("Failed to add contact.");
282
+ fastify.log.error({ err: error }, "xEmail addContact failed");
283
+ throw new Error(`[xEmail] Failed to add contact: ${error.message}`);
251
284
  }
252
285
  },
253
286
 
254
287
  /**
255
- * Search for a contact by email
288
+ * Search for a contact by email.
256
289
  * @param {string} email - Contact email
257
- * @returns {Promise<object>} Contact data
290
+ * @returns {Promise<{found: boolean, contact?: object}>}
258
291
  */
259
292
  searchContact: async (email) => {
293
+ if (!email || typeof email !== "string") {
294
+ throw new Error("[xEmail] 'email' (string) is required for searchContact().");
295
+ }
296
+
260
297
  try {
261
- const request = {
298
+ const [response, body] = await sgClient.request({
262
299
  url: `/v3/marketing/contacts/search/emails`,
263
300
  method: "POST",
264
301
  body: { emails: [email] },
265
- };
266
-
267
- const [response, body] = await sgClient.request(request);
302
+ });
268
303
 
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 };
304
+ if (response.statusCode === 200 && body.result?.[email]) {
305
+ return { found: true, contact: body.result[email].contact };
276
306
  }
307
+
308
+ return { found: false };
277
309
  } catch (error) {
278
- fastify.log.error("Email searchContact failed:", error);
279
- throw new Error("Failed to search contact.");
310
+ fastify.log.error({ err: error }, "xEmail searchContact failed");
311
+ throw new Error(`[xEmail] Failed to search contact: ${error.message}`);
280
312
  }
281
313
  },
282
314
 
283
315
  /**
284
- * Delete a contact by ID
316
+ * Delete a contact by ID.
285
317
  * @param {string} contactId - Contact ID
286
- * @returns {Promise<boolean>} Success status
318
+ * @returns {Promise<boolean>}
287
319
  */
288
320
  deleteContact: async (contactId) => {
321
+ if (!contactId || typeof contactId !== "string") {
322
+ throw new Error("[xEmail] 'contactId' (string) is required for deleteContact().");
323
+ }
324
+
289
325
  try {
290
- const request = {
326
+ const [response] = await sgClient.request({
291
327
  url: `/v3/marketing/contacts`,
292
328
  method: "DELETE",
293
329
  qs: { ids: contactId },
294
- };
295
-
296
- const [response] = await sgClient.request(request);
330
+ });
297
331
  return response.statusCode === 202 || response.statusCode === 200;
298
332
  } catch (error) {
299
- fastify.log.error("Email deleteContact failed:", error);
300
- throw new Error("Failed to delete contact.");
333
+ fastify.log.error({ err: error }, "xEmail deleteContact failed");
334
+ throw new Error(`[xEmail] Failed to delete contact: ${error.message}`);
301
335
  }
302
336
  },
303
337
 
304
338
  /**
305
- * Create a new contact list
339
+ * Create a new contact list.
306
340
  * @param {string} name - List name
307
- * @returns {Promise<object>} List object
341
+ * @returns {Promise<{success: boolean, list: object}>}
308
342
  */
309
343
  createList: async (name) => {
344
+ if (!name || typeof name !== "string") {
345
+ throw new Error("[xEmail] 'name' (string) is required for createList().");
346
+ }
347
+
310
348
  try {
311
- const request = {
349
+ const [, body] = await sgClient.request({
312
350
  url: `/v3/marketing/lists`,
313
351
  method: "POST",
314
352
  body: { name },
315
- };
316
-
317
- const [response, body] = await sgClient.request(request);
353
+ });
318
354
  return { success: true, list: body };
319
355
  } catch (error) {
320
- fastify.log.error("Email createList failed:", error);
321
- throw new Error("Failed to create list.");
356
+ fastify.log.error({ err: error }, "xEmail createList failed");
357
+ throw new Error(`[xEmail] Failed to create list: ${error.message}`);
322
358
  }
323
359
  },
324
360
 
325
361
  /**
326
- * Get all contact lists
327
- * @returns {Promise<object[]>} Array of lists
362
+ * Get all contact lists.
363
+ * @returns {Promise<object[]>}
328
364
  */
329
365
  getLists: async () => {
330
366
  try {
331
- const request = {
367
+ const [, body] = await sgClient.request({
332
368
  url: `/v3/marketing/lists`,
333
369
  method: "GET",
334
- };
335
-
336
- const [response, body] = await sgClient.request(request);
370
+ });
337
371
  return body.result || [];
338
372
  } catch (error) {
339
- fastify.log.error("Email getLists failed:", error);
340
- throw new Error("Failed to get lists.");
373
+ fastify.log.error({ err: error }, "xEmail getLists failed");
374
+ throw new Error(`[xEmail] Failed to get lists: ${error.message}`);
341
375
  }
342
376
  },
343
377
 
344
378
  /**
345
- * Delete a contact list
379
+ * Delete a contact list.
346
380
  * @param {string} listId - List ID
347
- * @returns {Promise<boolean>} Success status
381
+ * @returns {Promise<boolean>}
348
382
  */
349
383
  deleteList: async (listId) => {
384
+ if (!listId || typeof listId !== "string") {
385
+ throw new Error("[xEmail] 'listId' (string) is required for deleteList().");
386
+ }
387
+
350
388
  try {
351
- const request = {
389
+ const [response] = await sgClient.request({
352
390
  url: `/v3/marketing/lists/${listId}`,
353
391
  method: "DELETE",
354
- };
355
-
356
- const [response] = await sgClient.request(request);
357
- return response.statusCode === 202 || response.statusCode === 200 || response.statusCode === 204;
392
+ });
393
+ return [200, 202, 204].includes(response.statusCode);
358
394
  } catch (error) {
359
- fastify.log.error("Email deleteList failed:", error);
360
- throw new Error("Failed to delete list.");
395
+ fastify.log.error({ err: error }, "xEmail deleteList failed");
396
+ throw new Error(`[xEmail] Failed to delete list: ${error.message}`);
361
397
  }
362
398
  },
363
399
  });
@@ -365,4 +401,5 @@ async function xEmail(fastify, options) {
365
401
 
366
402
  export default fp(xEmail, {
367
403
  name: "xEmail",
404
+ fastify: ">=5.0.0",
368
405
  });