@xenterprises/fastify-xemail 1.1.0 → 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/LICENSE CHANGED
@@ -4,54 +4,94 @@ Copyright (c) 2024-2026 X Enterprises LLC. All Rights Reserved.
4
4
 
5
5
  This software and associated documentation files (the "Software") are the
6
6
  exclusive property of X Enterprises LLC, a Washington limited liability
7
- company.
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.
8
10
 
9
11
  TERMS AND CONDITIONS
10
12
 
11
13
  1. OWNERSHIP
12
14
  All rights, title, and interest in and to the Software, including all
13
15
  intellectual property rights, are and shall remain the exclusive property
14
- of X Enterprises LLC.
16
+ of X Enterprises. No rights are granted except as expressly set forth in
17
+ this License.
15
18
 
16
- 2. RESTRICTIONS
17
- Without the prior written consent of X Enterprises LLC, you may not:
18
- - Copy, modify, or distribute the Software
19
- - Reverse engineer, decompile, or disassemble the Software
20
- - Sublicense, sell, lease, or otherwise transfer the Software
21
- - Remove or alter any proprietary notices or labels
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
22
 
23
- 3. AUTHORIZED USE
24
- Use of this Software is limited to authorized employees, contractors, and
25
- agents of X Enterprises LLC, solely for purposes approved by X Enterprises
26
- LLC.
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.
27
63
 
28
64
  4. NO WARRANTY
29
65
  THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
66
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
67
  FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL
32
- X ENTERPRISES LLC BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY,
33
- WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF,
34
- OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
35
- SOFTWARE.
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.
36
72
 
37
73
  5. LIMITATION OF LIABILITY
38
- IN NO EVENT SHALL X ENTERPRISES LLC BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
39
- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
40
- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
41
- OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
42
- WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
43
- OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
44
- ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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.
45
81
 
46
82
  6. GOVERNING LAW
47
- This license shall be governed by and construed in accordance with the laws
48
- of the State of Washington, United States, without regard to its conflict
49
- of law provisions.
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.
50
88
 
51
89
  7. TERMINATION
52
- This license is effective until terminated. X Enterprises LLC may terminate
53
- this license at any time without notice. Upon termination, you must destroy
54
- all copies of the Software in your possession.
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.
55
95
 
56
96
  For licensing inquiries, contact: legal@x.enterprises
57
97
 
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.0",
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",
@@ -21,8 +27,9 @@
21
27
  "plugin"
22
28
  ],
23
29
  "author": "Tim Mushen",
24
- "license": "UNLICENSED",
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
  },
@@ -33,5 +40,8 @@
33
40
  },
34
41
  "peerDependencies": {
35
42
  "fastify": "^5.0.0"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
36
46
  }
37
47
  }
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
  });