@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 +35 -0
- package/LICENSE +69 -29
- package/README.md +69 -121
- package/package.json +14 -4
- package/src/xEmail.js +27 -9
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
|
|
16
|
+
of X Enterprises. No rights are granted except as expressly set forth in
|
|
17
|
+
this License.
|
|
15
18
|
|
|
16
|
-
2.
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
33
|
-
WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT
|
|
34
|
-
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
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
|
|
39
|
-
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
|
40
|
-
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
41
|
-
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
42
|
-
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
43
|
-
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
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
|
|
48
|
-
of the State of Washington, United States, without regard to its
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
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:
|
|
21
|
-
fromEmail:
|
|
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
|
-
|
|
49
|
+
Invalid options fail fast at registration with errors like:
|
|
43
50
|
|
|
44
|
-
|
|
51
|
+
```
|
|
52
|
+
xemail: option `apiKey` must be a string, e.g. `app.register(xEmail, { apiKey: 'SG.your-api-key' })`
|
|
53
|
+
```
|
|
45
54
|
|
|
46
|
-
|
|
55
|
+
## Decorators
|
|
47
56
|
|
|
48
|
-
|
|
57
|
+
The plugin adds one decorator: `fastify.xEmail`. No request decorators.
|
|
49
58
|
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
+
### `sendTemplate(to, subject, templateId, dynamicData?, extraOptions?)`
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
123
|
+
None. This plugin adds no routes.
|
|
186
124
|
|
|
187
|
-
|
|
125
|
+
## Error behavior
|
|
188
126
|
|
|
189
|
-
|
|
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
|
-
|
|
138
|
+
## Requirements
|
|
192
139
|
|
|
193
|
-
|
|
140
|
+
- Node.js >= 20
|
|
141
|
+
- Fastify ^5.0.0 (peer dependency)
|
|
194
142
|
|
|
195
143
|
## License
|
|
196
144
|
|
|
197
|
-
|
|
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.
|
|
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
|
-
".":
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"default": "./src/xEmail.js"
|
|
12
|
+
}
|
|
9
13
|
},
|
|
10
14
|
"scripts": {
|
|
11
|
-
"test": "node --test test
|
|
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": "
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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: "
|
|
404
|
-
fastify: "
|
|
421
|
+
name: "xemail",
|
|
422
|
+
fastify: "5.x",
|
|
405
423
|
});
|