@xenterprises/fastify-xemail 1.0.2 → 1.1.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/LICENSE +60 -0
- package/README.md +167 -35
- package/index.d.ts +86 -278
- package/package.json +4 -5
- package/src/xEmail.js +188 -151
package/LICENSE
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
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.
|
|
8
|
+
|
|
9
|
+
TERMS AND CONDITIONS
|
|
10
|
+
|
|
11
|
+
1. OWNERSHIP
|
|
12
|
+
All rights, title, and interest in and to the Software, including all
|
|
13
|
+
intellectual property rights, are and shall remain the exclusive property
|
|
14
|
+
of X Enterprises LLC.
|
|
15
|
+
|
|
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
|
|
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.
|
|
27
|
+
|
|
28
|
+
4. NO WARRANTY
|
|
29
|
+
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
30
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
31
|
+
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.
|
|
36
|
+
|
|
37
|
+
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.
|
|
45
|
+
|
|
46
|
+
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.
|
|
50
|
+
|
|
51
|
+
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.
|
|
55
|
+
|
|
56
|
+
For licensing inquiries, contact: legal@x.enterprises
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
X Enterprises LLC
|
|
60
|
+
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
|
|
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:
|
|
21
|
+
fromEmail: process.env.SENDGRID_FROM_EMAIL,
|
|
22
|
+
fromName: 'My App', // optional
|
|
32
23
|
});
|
|
33
24
|
|
|
34
|
-
//
|
|
35
|
-
fastify.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
bcc?: string[];
|
|
26
|
-
|
|
27
|
-
/** Custom categories for tracking */
|
|
11
|
+
bcc?: string | string[];
|
|
12
|
+
cc?: string | string[];
|
|
28
13
|
categories?: string[];
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
metadata?: Record<string, string>;
|
|
14
|
+
headers?: Record<string, string>;
|
|
15
|
+
[key: string]: unknown;
|
|
32
16
|
}
|
|
33
17
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
32
|
+
success: boolean;
|
|
33
|
+
statusCode: number;
|
|
62
34
|
messageId: string;
|
|
35
|
+
}
|
|
63
36
|
|
|
64
|
-
|
|
65
|
-
|
|
37
|
+
/** Result from sendBulk(). */
|
|
38
|
+
export interface EmailBulkResult {
|
|
39
|
+
success: boolean;
|
|
40
|
+
count: number;
|
|
41
|
+
statusCode: number;
|
|
42
|
+
}
|
|
66
43
|
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
62
|
+
/** Result from addContact(). */
|
|
63
|
+
export interface EmailAddContactResult {
|
|
64
|
+
success: boolean;
|
|
65
|
+
jobId: string;
|
|
66
|
+
email: string;
|
|
81
67
|
}
|
|
82
68
|
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
id: string;
|
|
69
|
+
/** Result from searchContact(). */
|
|
70
|
+
export interface EmailSearchContactResult {
|
|
71
|
+
found: boolean;
|
|
72
|
+
contact?: Record<string, unknown>;
|
|
73
|
+
}
|
|
89
74
|
|
|
90
|
-
|
|
91
|
-
|
|
75
|
+
/** Result from createList(). */
|
|
76
|
+
export interface EmailCreateListResult {
|
|
77
|
+
success: boolean;
|
|
78
|
+
list: Record<string, unknown>;
|
|
79
|
+
}
|
|
92
80
|
|
|
93
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
5
|
-
"description": "Fastify plugin for SendGrid email integration.",
|
|
4
|
+
"version": "1.1.0",
|
|
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": "
|
|
24
|
+
"license": "UNLICENSED",
|
|
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",
|
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
|
-
|
|
11
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
32
|
-
* @param {object} extraOptions - Additional SendGrid options
|
|
33
|
-
* @returns {Promise<
|
|
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
|
|
51
|
+
from,
|
|
40
52
|
subject,
|
|
41
53
|
html,
|
|
42
|
-
text: text || html.replace(/<[^>]*>/g, ""),
|
|
54
|
+
text: text || html.replace(/<[^>]*>/g, ""),
|
|
43
55
|
...extraOptions,
|
|
44
56
|
};
|
|
45
57
|
|
|
46
58
|
const response = await sgMail.send(msg);
|
|
47
|
-
return {
|
|
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("
|
|
50
|
-
throw new Error(
|
|
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<
|
|
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
|
|
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 {
|
|
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("
|
|
78
|
-
throw new Error(
|
|
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
|
|
88
|
-
* @returns {Promise<
|
|
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
|
|
125
|
+
from,
|
|
95
126
|
subject,
|
|
96
127
|
html,
|
|
97
128
|
attachments: attachments.map((att) => ({
|
|
98
|
-
content: att.content,
|
|
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 {
|
|
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("
|
|
109
|
-
throw new Error(
|
|
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
|
|
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<
|
|
153
|
+
* @returns {Promise<{success: boolean, count: number, statusCode: number}>}
|
|
119
154
|
*/
|
|
120
155
|
sendBulk: async (to, subject, html) => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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("
|
|
133
|
-
throw new Error(
|
|
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
|
|
139
|
-
* @param {Array<{to: string, subject: string, html: string}>} messages
|
|
140
|
-
* @returns {Promise<
|
|
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
|
-
|
|
144
|
-
|
|
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>}
|
|
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
|
|
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("
|
|
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 (
|
|
216
|
-
* @param {string[]} listIds -
|
|
217
|
-
* @returns {Promise<
|
|
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
|
|
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("
|
|
250
|
-
throw new Error(
|
|
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>}
|
|
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
|
|
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
|
|
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("
|
|
279
|
-
throw new Error(
|
|
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>}
|
|
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
|
|
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("
|
|
300
|
-
throw new Error(
|
|
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>}
|
|
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
|
|
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("
|
|
321
|
-
throw new Error(
|
|
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[]>}
|
|
362
|
+
* Get all contact lists.
|
|
363
|
+
* @returns {Promise<object[]>}
|
|
328
364
|
*/
|
|
329
365
|
getLists: async () => {
|
|
330
366
|
try {
|
|
331
|
-
const
|
|
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("
|
|
340
|
-
throw new Error(
|
|
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>}
|
|
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
|
|
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("
|
|
360
|
-
throw new Error(
|
|
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
|
});
|