@xenterprises/fastify-xemail 1.1.1 → 1.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@xenterprises/fastify-xemail` are documented here.
4
+
5
+ ## Unreleased
6
+
7
+ ## [1.2.0] - 2026-07-27
8
+
9
+ ### Breaking changes
10
+
11
+ - **Registration error messages changed format.** Missing/invalid `apiKey` and
12
+ `fromEmail` now throw `xemail: option \`<name>\` must be a string, e.g.
13
+ \`app.register(xEmail, { ... })\`` instead of `[xEmail] '<name>' (string) is required.`
14
+ (Suite-wide error-format standard; code matching on the old message text must be updated.)
15
+ - **`fp()` metadata changed**: plugin name is now `xemail` (was `xEmail`) and the
16
+ Fastify constraint is `5.x` (was `>=5.0.0`). Affects duplicate-registration
17
+ detection and `fastify-plugin` metadata only.
18
+
19
+ ### Added
20
+
21
+ - TypeScript declarations are now reachable by consumers: `package.json` gains a
22
+ top-level `"types": "./index.d.ts"` and a `types` condition in `exports["."]`.
23
+ Declaration content is unchanged.
24
+ - New fail-fast validation: `fromName` must be a string when provided and `active`
25
+ must be a boolean when provided.
26
+ - Tests asserting the plugin is env-independent (no `process.env` reads; options
27
+ are the only configuration source).
28
+
29
+ ### Changed
30
+
31
+ - Tooling migrated to Biome 2.5.5 (`lint`/`format` scripts); test script now uses
32
+ the `test/**/*.test.js` glob.
33
+ - README rewritten to the suite README contract; `docs/INTEGRATION.md` examples
34
+ corrected to use the `fastify.xEmail` decorator.
35
+ - Dependency updates via `npm audit fix` — 0 known vulnerabilities.
package/README.md CHANGED
@@ -1,14 +1,19 @@
1
1
  # @xenterprises/fastify-xemail
2
2
 
3
- Fastify plugin for SendGrid email — send transactional emails, template emails, bulk messages, validate addresses, and manage marketing contacts and lists.
3
+ Fastify 5 plugin for SendGrid — send transactional, template, attachment, and bulk emails, validate addresses, and manage SendGrid Marketing contacts and lists, all through one `fastify.xEmail` decorator. For Fastify apps that need email with the least possible wiring.
4
4
 
5
- ## Installation
5
+ ## Install
6
6
 
7
7
  ```bash
8
- npm install @xenterprises/fastify-xemail
8
+ npm install @xenterprises/fastify-xemail fastify@5
9
9
  ```
10
10
 
11
- ## Usage
11
+ `fastify@^5.0.0` is a peer dependency.
12
+
13
+ TypeScript declarations ship with the package — importing the plugin types the
14
+ `fastify.xEmail` decorator automatically (`index.d.ts`).
15
+
16
+ ## Minimal example
12
17
 
13
18
  ```javascript
14
19
  import Fastify from 'fastify';
@@ -17,12 +22,10 @@ import xEmail from '@xenterprises/fastify-xemail';
17
22
  const fastify = Fastify();
18
23
 
19
24
  await fastify.register(xEmail, {
20
- apiKey: process.env.SENDGRID_API_KEY,
21
- fromEmail: process.env.SENDGRID_FROM_EMAIL,
22
- fromName: 'My App', // optional
25
+ apiKey: 'SG.your-api-key',
26
+ fromEmail: 'noreply@example.com',
23
27
  });
24
28
 
25
- // Send a simple email
26
29
  await fastify.xEmail.send(
27
30
  'user@example.com',
28
31
  'Welcome!',
@@ -30,168 +33,113 @@ await fastify.xEmail.send(
30
33
  );
31
34
  ```
32
35
 
36
+ The plugin reads **no environment variables**. All configuration arrives via the
37
+ register options object; reading `process.env.SENDGRID_API_KEY` and passing it in
38
+ is the consumer's job.
39
+
33
40
  ## Options
34
41
 
35
42
  | Name | Type | Default | Required | Description |
36
43
  |------|------|---------|----------|-------------|
37
- | `apiKey` | `string` | — | Yes | SendGrid API key |
44
+ | `apiKey` | `string` | — | Yes | SendGrid API key (needs Mail Send; Marketing APIs for contact/list methods) |
38
45
  | `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 |
46
+ | `fromName` | `string` | — | No | Sender display name; when set, `from` becomes `{ email, name }` |
47
+ | `active` | `boolean` | `true` | No | Set `false` to disable the plugin entirely (no decorator is added) |
41
48
 
42
- ## Decorated Methods
49
+ Invalid options fail fast at registration with errors like:
43
50
 
44
- All methods are available on `fastify.xEmail`.
51
+ ```
52
+ xemail: option `apiKey` must be a string, e.g. `app.register(xEmail, { apiKey: 'SG.your-api-key' })`
53
+ ```
45
54
 
46
- ### `send(to, subject, html, text?, extraOptions?)`
55
+ ## Decorators
47
56
 
48
- Send an email. Plain text is auto-generated from HTML if omitted.
57
+ The plugin adds one decorator: `fastify.xEmail`. No request decorators.
49
58
 
50
- ```javascript
51
- const result = await fastify.xEmail.send('user@example.com', 'Hello', '<p>Hi</p>');
52
- // { success: true, statusCode: 202, messageId: 'xxx' }
53
- ```
59
+ ### `send(to, subject, html, text?, extraOptions?)`
54
60
 
55
- ### `sendTemplate(to, subject, templateId, dynamicData?, extraOptions?)`
61
+ Send an email. Plain text is auto-generated from the HTML if `text` is omitted.
62
+ `extraOptions` is merged into the SendGrid message (e.g. `replyTo`, `cc`, `categories`).
63
+ Returns `{ success, statusCode, messageId }`.
56
64
 
57
- Send using a SendGrid dynamic template.
65
+ ### `sendTemplate(to, subject, templateId, dynamicData?, extraOptions?)`
58
66
 
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
+ Send using a SendGrid dynamic template (`templateId` is the `d-xxx` ID). `dynamicData`
68
+ is sent as `dynamicTemplateData` (the subject is always included). Returns
69
+ `{ success, statusCode, messageId }`.
67
70
 
68
71
  ### `sendWithAttachments(to, subject, html, attachments)`
69
72
 
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
- ```
73
+ Send an email with attachments. Each attachment needs `content` (base64), `filename`,
74
+ and `type` (MIME type); `disposition` defaults to `'attachment'`. Returns
75
+ `{ success, statusCode, messageId }`.
80
76
 
81
77
  ### `sendBulk(to, subject, html)`
82
78
 
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
- ```
79
+ Send the same email to an array of recipients in one call (`sgMail.sendMultiple`).
80
+ Returns `{ success, count, statusCode }`.
93
81
 
94
82
  ### `sendPersonalizedBulk(messages)`
95
83
 
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 }, ...]
104
- ```
84
+ Send different content per recipient. `messages` is an array of
85
+ `{ to, subject, html, text? }`. Uses `Promise.allSettled`, so it never throws on
86
+ individual failures — returns one result per recipient:
87
+ `{ success: true, to, statusCode }` or `{ success: false, to, error }`.
105
88
 
106
89
  ### `validate(email)`
107
90
 
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
- ```
91
+ Validate an address via the SendGrid Email Validation API. Returns
92
+ `{ email, valid, verdict, score, result }`. On API failure it returns a soft result
93
+ `{ email, valid: false, verdict: 'Unknown', error }` instead of throwing.
114
94
 
115
95
  ### `addContact(email, data?, listIds?)`
116
96
 
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
- ```
97
+ Add or update a SendGrid Marketing contact. `data` accepts `firstName`/`lastName`
98
+ (or `first_name`/`last_name`) and `customFields` (spread into the contact);
99
+ `listIds` is an array of list IDs. Returns `{ success, jobId, email }`.
127
100
 
128
101
  ### `searchContact(email)`
129
102
 
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
- ```
103
+ Search for a contact by email. Returns `{ found: true, contact }` or `{ found: false }`.
136
104
 
137
105
  ### `deleteContact(contactId)`
138
106
 
139
- Delete a contact by ID. Returns `true` on success.
107
+ Delete a contact by ID. Returns `true` on success (HTTP 200/202), `false` otherwise.
140
108
 
141
109
  ### `createList(name)`
142
110
 
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
- ```
111
+ Create a marketing contact list. Returns `{ success, list }`.
149
112
 
150
113
  ### `getLists()`
151
114
 
152
- Get all contact lists. Returns an array.
115
+ Get all contact lists. Returns an array (empty when none exist).
153
116
 
154
117
  ### `deleteList(listId)`
155
118
 
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.
119
+ Delete a list by ID. Returns `true` on success (HTTP 200/202/204), `false` otherwise.
169
120
 
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 |
121
+ ## Routes
184
122
 
185
- ## How It Works
123
+ None. This plugin adds no routes.
186
124
 
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.
125
+ ## Error behavior
188
126
 
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.
127
+ - **Registration** throws fail-fast `Error`s naming the plugin and the option, with a
128
+ registration example (see Options above).
129
+ - **Method argument validation** throws synchronously-rejecting `Error`s prefixed with
130
+ `[xEmail]`, e.g. `[xEmail] 'to' is required for send().`
131
+ - **SendGrid API failures** are logged via `fastify.log.error` (structured error, never
132
+ the API key) and re-thrown wrapped: `[xEmail] Failed to send email: <reason>`.
133
+ `validate()` is the exception — it returns a soft result instead of throwing, since
134
+ validation failures are expected in normal operation.
135
+ - With `active: false` the plugin returns before registering anything, so
136
+ `fastify.xEmail` is `undefined`.
190
137
 
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.
138
+ ## Requirements
192
139
 
193
- Setting `active: false` causes the plugin to return immediately without decorating, which is useful for disabling email in test environments.
140
+ - Node.js >= 20
141
+ - Fastify ^5.0.0 (peer dependency)
194
142
 
195
143
  ## License
196
144
 
197
- UNLICENSED
145
+ Proprietary — All Rights Reserved, X Enterprises. See `LICENSE`.
package/package.json CHANGED
@@ -1,14 +1,20 @@
1
1
  {
2
2
  "name": "@xenterprises/fastify-xemail",
3
3
  "type": "module",
4
- "version": "1.1.1",
4
+ "version": "1.2.0",
5
5
  "description": "Fastify plugin for SendGrid email integration — transactional emails, templates, bulk sending, validation, and contact management.",
6
6
  "main": "src/xEmail.js",
7
+ "types": "./index.d.ts",
7
8
  "exports": {
8
- ".": "./src/xEmail.js"
9
+ ".": {
10
+ "types": "./index.d.ts",
11
+ "default": "./src/xEmail.js"
12
+ }
9
13
  },
10
14
  "scripts": {
11
- "test": "node --test test/xEmail.test.js"
15
+ "test": "node --test 'test/**/*.test.js'",
16
+ "lint": "biome check src/ test/",
17
+ "format": "biome format --write src/ test/"
12
18
  },
13
19
  "engines": {
14
20
  "node": ">=20.0.0",
@@ -23,6 +29,7 @@
23
29
  "author": "Tim Mushen",
24
30
  "license": "SEE LICENSE IN LICENSE",
25
31
  "devDependencies": {
32
+ "@biomejs/biome": "2.5.5",
26
33
  "@types/node": "^22.7.4",
27
34
  "fastify": "^5.1.0"
28
35
  },
package/src/xEmail.js CHANGED
@@ -1,6 +1,6 @@
1
- import fp from "fastify-plugin";
2
- import sgMail from "@sendgrid/mail";
3
1
  import sgClient from "@sendgrid/client";
2
+ import sgMail from "@sendgrid/mail";
3
+ import fp from "fastify-plugin";
4
4
 
5
5
  /**
6
6
  * @param {import('fastify').FastifyInstance} fastify
@@ -15,12 +15,26 @@ async function xEmail(fastify, options) {
15
15
 
16
16
  if (active === false) return;
17
17
 
18
+ if (typeof active !== "boolean") {
19
+ throw new Error("xemail: option `active` must be a boolean");
20
+ }
21
+
18
22
  if (!apiKey || typeof apiKey !== "string") {
19
- throw new Error("[xEmail] 'apiKey' (string) is required.");
23
+ throw new Error(
24
+ "xemail: option `apiKey` must be a string, e.g. `app.register(xEmail, { apiKey: 'SG.your-api-key' })`"
25
+ );
20
26
  }
21
27
 
22
28
  if (!fromEmail || typeof fromEmail !== "string") {
23
- throw new Error("[xEmail] 'fromEmail' (string) is required.");
29
+ throw new Error(
30
+ "xemail: option `fromEmail` must be a string, e.g. `app.register(xEmail, { apiKey: 'SG.your-api-key', fromEmail: 'noreply@example.com' })`"
31
+ );
32
+ }
33
+
34
+ if (fromName !== undefined && typeof fromName !== "string") {
35
+ throw new Error(
36
+ "xemail: option `fromName` must be a string, e.g. `app.register(xEmail, { apiKey: 'SG.your-api-key', fromEmail: 'noreply@example.com', fromName: 'My App' })`"
37
+ );
24
38
  }
25
39
 
26
40
  sgMail.setApiKey(apiKey);
@@ -176,7 +190,9 @@ async function xEmail(fastify, options) {
176
190
  */
177
191
  sendPersonalizedBulk: async (messages) => {
178
192
  if (!Array.isArray(messages) || messages.length === 0) {
179
- throw new Error("[xEmail] 'messages' must be a non-empty array for sendPersonalizedBulk().");
193
+ throw new Error(
194
+ "[xEmail] 'messages' must be a non-empty array for sendPersonalizedBulk()."
195
+ );
180
196
  }
181
197
 
182
198
  const mailMessages = messages.map((msg) => ({
@@ -232,7 +248,9 @@ async function xEmail(fastify, options) {
232
248
  };
233
249
  }
234
250
 
235
- throw new Error(body.errors ? body.errors.map((e) => e.message).join(", ") : "Validation failed");
251
+ throw new Error(
252
+ body.errors ? body.errors.map((e) => e.message).join(", ") : "Validation failed"
253
+ );
236
254
  } catch (error) {
237
255
  fastify.log.error({ err: error }, "xEmail validate failed");
238
256
  return {
@@ -277,7 +295,7 @@ async function xEmail(fastify, options) {
277
295
  return { success: true, jobId: body.job_id, email };
278
296
  }
279
297
 
280
- throw new Error("Unexpected status code: " + response.statusCode);
298
+ throw new Error(`Unexpected status code: ${response.statusCode}`);
281
299
  } catch (error) {
282
300
  fastify.log.error({ err: error }, "xEmail addContact failed");
283
301
  throw new Error(`[xEmail] Failed to add contact: ${error.message}`);
@@ -400,6 +418,6 @@ async function xEmail(fastify, options) {
400
418
  }
401
419
 
402
420
  export default fp(xEmail, {
403
- name: "xEmail",
404
- fastify: ">=5.0.0",
421
+ name: "xemail",
422
+ fastify: "5.x",
405
423
  });