@geekmidas/emailkit 9.0.2 → 10.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/{client-BrzAoy26.d.mts → client-LvoMlKEk.d.mts} +2 -2
  2. package/dist/{client-BrzAoy26.d.mts.map → client-LvoMlKEk.d.mts.map} +1 -1
  3. package/dist/client.d.mts +2 -2
  4. package/dist/errors-6difZDw0.cjs +68 -0
  5. package/dist/errors-6difZDw0.cjs.map +1 -0
  6. package/dist/errors-Ba3cEHPO.mjs +50 -0
  7. package/dist/errors-Ba3cEHPO.mjs.map +1 -0
  8. package/dist/errors-ZllQgZ9Y.d.mts +41 -0
  9. package/dist/errors-ZllQgZ9Y.d.mts.map +1 -0
  10. package/dist/errors-xJ88z_5r.d.cts +41 -0
  11. package/dist/errors-xJ88z_5r.d.cts.map +1 -0
  12. package/dist/errors.cjs +5 -0
  13. package/dist/errors.d.cts +2 -0
  14. package/dist/errors.d.mts +2 -0
  15. package/dist/errors.mjs +3 -0
  16. package/dist/index.cjs +7 -1
  17. package/dist/index.d.cts +4 -2
  18. package/dist/index.d.mts +5 -3
  19. package/dist/index.mjs +3 -1
  20. package/dist/{types-FrE6exSX.d.mts → types-BE4Kh3y6.d.mts} +1 -1
  21. package/dist/{types-FrE6exSX.d.mts.map → types-BE4Kh3y6.d.mts.map} +1 -1
  22. package/dist/types.d.mts +1 -1
  23. package/dist/url-B8QqBrMt.mjs +49 -0
  24. package/dist/url-B8QqBrMt.mjs.map +1 -0
  25. package/dist/url-Cd5VhxhW.d.mts +20 -0
  26. package/dist/url-Cd5VhxhW.d.mts.map +1 -0
  27. package/dist/url-Cf508-Ji.d.cts +20 -0
  28. package/dist/url-Cf508-Ji.d.cts.map +1 -0
  29. package/dist/url-GT3jLGq-.cjs +54 -0
  30. package/dist/url-GT3jLGq-.cjs.map +1 -0
  31. package/dist/url.cjs +4 -0
  32. package/dist/url.d.cts +3 -0
  33. package/dist/url.d.mts +3 -0
  34. package/dist/url.mjs +4 -0
  35. package/package.json +15 -4
  36. package/CHANGELOG.md +0 -40
  37. package/src/__tests__/client.spec.ts +0 -372
  38. package/src/__tests__/client.spec.tsx +0 -185
  39. package/src/client.ts +0 -90
  40. package/src/index.ts +0 -11
  41. package/src/types.ts +0 -99
  42. package/tsconfig.json +0 -10
@@ -1,372 +0,0 @@
1
- import type { ReactElement } from 'react';
2
- import { beforeEach, describe, expect, it, vi } from 'vitest';
3
- import { createEmailClient, SMTPClient } from '../client';
4
- import type { EmailClientConfig, TemplateRecord } from '../types';
5
-
6
- // Mock nodemailer
7
- vi.mock('nodemailer', () => ({
8
- default: {
9
- createTransport: vi.fn(() => ({
10
- sendMail: vi.fn(),
11
- verify: vi.fn(),
12
- close: vi.fn(),
13
- })),
14
- },
15
- }));
16
-
17
- // Mock @react-email/components
18
- vi.mock('@react-email/components', () => ({
19
- render: vi.fn(),
20
- }));
21
-
22
- import { render } from '@react-email/components';
23
- // Import mocked modules
24
- import nodemailer from 'nodemailer';
25
-
26
- // Test templates
27
- interface WelcomeProps {
28
- name: string;
29
- verificationUrl: string;
30
- }
31
-
32
- interface ResetPasswordProps {
33
- resetUrl: string;
34
- expiresIn: number;
35
- }
36
-
37
- const WelcomeTemplate = (props: WelcomeProps): ReactElement =>
38
- ({ type: 'div', props: { children: `Welcome ${props.name}` } }) as any;
39
-
40
- const ResetPasswordTemplate = (props: ResetPasswordProps): ReactElement =>
41
- ({ type: 'div', props: { children: `Reset: ${props.resetUrl}` } }) as any;
42
-
43
- const templates = {
44
- welcome: WelcomeTemplate,
45
- resetPassword: ResetPasswordTemplate,
46
- } satisfies TemplateRecord;
47
-
48
- type TestTemplates = typeof templates;
49
-
50
- describe('SMTPClient', () => {
51
- let mockTransporter: {
52
- sendMail: ReturnType<typeof vi.fn>;
53
- verify: ReturnType<typeof vi.fn>;
54
- close: ReturnType<typeof vi.fn>;
55
- };
56
- let config: EmailClientConfig<TestTemplates>;
57
-
58
- beforeEach(() => {
59
- vi.clearAllMocks();
60
-
61
- mockTransporter = {
62
- sendMail: vi.fn().mockResolvedValue({
63
- messageId: 'test-message-id',
64
- accepted: ['recipient@example.com'],
65
- rejected: [],
66
- response: '250 OK',
67
- }),
68
- verify: vi.fn().mockResolvedValue(true),
69
- close: vi.fn(),
70
- };
71
-
72
- vi.mocked(nodemailer.createTransport).mockReturnValue(
73
- mockTransporter as any,
74
- );
75
-
76
- vi.mocked(render).mockResolvedValue('<html>Rendered HTML</html>');
77
-
78
- config = {
79
- smtp: {
80
- host: 'smtp.example.com',
81
- port: 587,
82
- secure: false,
83
- auth: {
84
- user: 'test@example.com',
85
- pass: 'password',
86
- },
87
- },
88
- templates,
89
- defaults: {
90
- from: 'noreply@example.com',
91
- },
92
- };
93
- });
94
-
95
- describe('constructor', () => {
96
- it('should create a transporter with SMTP config', () => {
97
- new SMTPClient(config);
98
-
99
- expect(nodemailer.createTransport).toHaveBeenCalledWith(config.smtp);
100
- });
101
- });
102
-
103
- describe('send', () => {
104
- it('should send an email with required options', async () => {
105
- const client = new SMTPClient(config);
106
-
107
- const result = await client.send({
108
- to: 'recipient@example.com',
109
- subject: 'Test Subject',
110
- text: 'Test content',
111
- });
112
-
113
- expect(mockTransporter.sendMail).toHaveBeenCalledWith({
114
- from: 'noreply@example.com',
115
- to: 'recipient@example.com',
116
- subject: 'Test Subject',
117
- text: 'Test content',
118
- });
119
-
120
- expect(result).toEqual({
121
- messageId: 'test-message-id',
122
- accepted: ['recipient@example.com'],
123
- rejected: [],
124
- response: '250 OK',
125
- });
126
- });
127
-
128
- it('should send an email with HTML content', async () => {
129
- const client = new SMTPClient(config);
130
-
131
- await client.send({
132
- to: 'recipient@example.com',
133
- subject: 'HTML Email',
134
- html: '<h1>Hello</h1>',
135
- });
136
-
137
- expect(mockTransporter.sendMail).toHaveBeenCalledWith(
138
- expect.objectContaining({
139
- html: '<h1>Hello</h1>',
140
- }),
141
- );
142
- });
143
-
144
- it('should override default from address', async () => {
145
- const client = new SMTPClient(config);
146
-
147
- await client.send({
148
- from: 'custom@example.com',
149
- to: 'recipient@example.com',
150
- subject: 'Custom From',
151
- text: 'Content',
152
- });
153
-
154
- expect(mockTransporter.sendMail).toHaveBeenCalledWith(
155
- expect.objectContaining({
156
- from: 'custom@example.com',
157
- }),
158
- );
159
- });
160
-
161
- it('should throw error when from is not provided and no default', async () => {
162
- const configNoDefaults: EmailClientConfig<TestTemplates> = {
163
- ...config,
164
- defaults: {},
165
- };
166
- const client = new SMTPClient(configNoDefaults);
167
-
168
- await expect(
169
- client.send({
170
- to: 'recipient@example.com',
171
- subject: 'No From',
172
- text: 'Content',
173
- }),
174
- ).rejects.toThrow(
175
- 'The "from" field is required in email options or defaults',
176
- );
177
- });
178
-
179
- it('should throw error when neither text nor html is provided', async () => {
180
- const client = new SMTPClient(config);
181
-
182
- await expect(
183
- client.send({
184
- to: 'recipient@example.com',
185
- subject: 'No Content',
186
- }),
187
- ).rejects.toThrow('Either text or html content must be provided');
188
- });
189
-
190
- it('should handle multiple recipients', async () => {
191
- const client = new SMTPClient(config);
192
-
193
- await client.send({
194
- to: ['user1@example.com', 'user2@example.com'],
195
- cc: 'cc@example.com',
196
- bcc: ['bcc1@example.com', 'bcc2@example.com'],
197
- subject: 'Multiple Recipients',
198
- text: 'Content',
199
- });
200
-
201
- expect(mockTransporter.sendMail).toHaveBeenCalledWith(
202
- expect.objectContaining({
203
- to: ['user1@example.com', 'user2@example.com'],
204
- cc: 'cc@example.com',
205
- bcc: ['bcc1@example.com', 'bcc2@example.com'],
206
- }),
207
- );
208
- });
209
-
210
- it('should handle rejected recipients', async () => {
211
- mockTransporter.sendMail.mockResolvedValue({
212
- messageId: 'test-id',
213
- accepted: ['valid@example.com'],
214
- rejected: ['invalid@example.com'],
215
- response: '250 OK',
216
- });
217
-
218
- const client = new SMTPClient(config);
219
-
220
- const result = await client.send({
221
- to: ['valid@example.com', 'invalid@example.com'],
222
- subject: 'Test',
223
- text: 'Content',
224
- });
225
-
226
- expect(result.accepted).toEqual(['valid@example.com']);
227
- expect(result.rejected).toEqual(['invalid@example.com']);
228
- });
229
-
230
- it('should handle missing accepted/rejected in response', async () => {
231
- mockTransporter.sendMail.mockResolvedValue({
232
- messageId: 'test-id',
233
- response: '250 OK',
234
- });
235
-
236
- const client = new SMTPClient(config);
237
-
238
- const result = await client.send({
239
- to: 'test@example.com',
240
- subject: 'Test',
241
- text: 'Content',
242
- });
243
-
244
- expect(result.accepted).toEqual([]);
245
- expect(result.rejected).toEqual([]);
246
- });
247
- });
248
-
249
- describe('sendTemplate', () => {
250
- it('should render and send a template', async () => {
251
- const client = new SMTPClient(config);
252
-
253
- const result = await client.sendTemplate('welcome', {
254
- to: 'user@example.com',
255
- subject: 'Welcome!',
256
- props: {
257
- name: 'John',
258
- verificationUrl: 'https://example.com/verify',
259
- },
260
- });
261
-
262
- expect(render).toHaveBeenCalled();
263
- expect(mockTransporter.sendMail).toHaveBeenCalledWith(
264
- expect.objectContaining({
265
- to: 'user@example.com',
266
- subject: 'Welcome!',
267
- html: '<html>Rendered HTML</html>',
268
- }),
269
- );
270
- expect(result.messageId).toBe('test-message-id');
271
- });
272
-
273
- it('should render resetPassword template', async () => {
274
- const client = new SMTPClient(config);
275
-
276
- await client.sendTemplate('resetPassword', {
277
- to: 'user@example.com',
278
- subject: 'Reset Your Password',
279
- props: {
280
- resetUrl: 'https://example.com/reset/abc123',
281
- expiresIn: 3600,
282
- },
283
- });
284
-
285
- expect(render).toHaveBeenCalled();
286
- expect(mockTransporter.sendMail).toHaveBeenCalled();
287
- });
288
-
289
- it('should throw error for unknown template', async () => {
290
- const client = new SMTPClient(config);
291
-
292
- await expect(
293
- client.sendTemplate('nonexistent' as any, {
294
- to: 'user@example.com',
295
- subject: 'Test',
296
- props: {},
297
- }),
298
- ).rejects.toThrow('Template "nonexistent" not found');
299
- });
300
- });
301
-
302
- describe('verify', () => {
303
- it('should return true when connection is valid', async () => {
304
- mockTransporter.verify.mockResolvedValue(true);
305
- const client = new SMTPClient(config);
306
-
307
- const result = await client.verify();
308
-
309
- expect(result).toBe(true);
310
- expect(mockTransporter.verify).toHaveBeenCalled();
311
- });
312
-
313
- it('should return false when connection fails', async () => {
314
- mockTransporter.verify.mockRejectedValue(new Error('Connection failed'));
315
- const client = new SMTPClient(config);
316
-
317
- const result = await client.verify();
318
-
319
- expect(result).toBe(false);
320
- });
321
- });
322
-
323
- describe('close', () => {
324
- it('should close the transporter', async () => {
325
- const client = new SMTPClient(config);
326
-
327
- await client.close();
328
-
329
- expect(mockTransporter.close).toHaveBeenCalled();
330
- });
331
- });
332
-
333
- describe('getTemplateNames', () => {
334
- it('should return all template names', () => {
335
- const client = new SMTPClient(config);
336
-
337
- const names = client.getTemplateNames();
338
-
339
- expect(names).toContain('welcome');
340
- expect(names).toContain('resetPassword');
341
- expect(names).toHaveLength(2);
342
- });
343
- });
344
- });
345
-
346
- describe('createEmailClient', () => {
347
- beforeEach(() => {
348
- vi.clearAllMocks();
349
- vi.mocked(nodemailer.createTransport).mockReturnValue({
350
- sendMail: vi.fn(),
351
- verify: vi.fn(),
352
- close: vi.fn(),
353
- } as any);
354
- });
355
-
356
- it('should create an SMTPClient instance', () => {
357
- const config: EmailClientConfig<TestTemplates> = {
358
- smtp: {
359
- host: 'smtp.example.com',
360
- port: 587,
361
- },
362
- templates,
363
- defaults: {
364
- from: 'test@example.com',
365
- },
366
- };
367
-
368
- const client = createEmailClient(config);
369
-
370
- expect(client).toBeInstanceOf(SMTPClient);
371
- });
372
- });
@@ -1,185 +0,0 @@
1
- // biome-ignore lint/correctness/noUnusedImports: <explanation>
2
- import React from 'react';
3
- import { beforeEach, describe, expect, it, vi } from 'vitest';
4
- import { createEmailClient, type SMTPClient } from '../client';
5
- import type { EmailClientConfig } from '../types';
6
-
7
- // Mock nodemailer
8
- vi.mock('nodemailer', () => ({
9
- default: {
10
- createTransport: vi.fn(() => ({
11
- sendMail: vi.fn().mockResolvedValue({
12
- messageId: 'test-message-id',
13
- accepted: ['test@example.com'],
14
- rejected: [],
15
- response: '250 OK',
16
- }),
17
- verify: vi.fn().mockResolvedValue(true),
18
- close: vi.fn(),
19
- })),
20
- },
21
- }));
22
-
23
- const TestTemplate = ({ name }: { name: string }) => <div>Hello {name}!</div>;
24
-
25
- const AnotherTemplate = ({
26
- title,
27
- message,
28
- }: {
29
- title: string;
30
- message: string;
31
- }) => (
32
- <div>
33
- <h1>{title}</h1>
34
- <p>{message}</p>
35
- </div>
36
- );
37
-
38
- describe('SMTPClient', () => {
39
- let client: SMTPClient<typeof templates>;
40
-
41
- const templates = {
42
- test: TestTemplate,
43
- another: AnotherTemplate,
44
- };
45
-
46
- const config: EmailClientConfig<typeof templates> = {
47
- smtp: {
48
- host: 'smtp.example.com',
49
- port: 587,
50
- auth: {
51
- user: 'test@example.com',
52
- pass: 'password',
53
- },
54
- },
55
- templates,
56
- defaults: {
57
- from: 'noreply@example.com',
58
- },
59
- };
60
-
61
- beforeEach(() => {
62
- client = createEmailClient(config);
63
- });
64
-
65
- describe('send', () => {
66
- it('should send plain text email', async () => {
67
- const result = await client.send({
68
- from: 'sender@example.com',
69
- to: 'recipient@example.com',
70
- subject: 'Test Email',
71
- text: 'This is a test email',
72
- });
73
-
74
- expect(result).toEqual({
75
- messageId: 'test-message-id',
76
- accepted: ['test@example.com'],
77
- rejected: [],
78
- response: '250 OK',
79
- });
80
- });
81
-
82
- it('should send HTML email', async () => {
83
- const result = await client.send({
84
- from: 'sender@example.com',
85
- to: 'recipient@example.com',
86
- subject: 'Test Email',
87
- html: '<p>This is a test email</p>',
88
- });
89
-
90
- expect(result.messageId).toBe('test-message-id');
91
- });
92
-
93
- it('should use default from address', async () => {
94
- const result = await client.send({
95
- to: 'recipient@example.com',
96
- subject: 'Test Email',
97
- text: 'This is a test email',
98
- from: {
99
- name: 'Test Sender',
100
- address: 'sender@example.com',
101
- },
102
- });
103
-
104
- expect(result.messageId).toBe('test-message-id');
105
- });
106
-
107
- it('should throw error if no content provided', async () => {
108
- await expect(
109
- client.send({
110
- from: 'sender@example.com',
111
- to: 'recipient@example.com',
112
- subject: 'Test Email',
113
- }),
114
- ).rejects.toThrow('Either text or html content must be provided');
115
- });
116
-
117
- it('should support multiple recipients', async () => {
118
- const result = await client.send({
119
- from: 'sender@example.com',
120
- to: ['recipient1@example.com', 'recipient2@example.com'],
121
- subject: 'Test Email',
122
- text: 'This is a test email',
123
- });
124
-
125
- expect(result.messageId).toBe('test-message-id');
126
- });
127
-
128
- it('should support CC and BCC', async () => {
129
- const result = await client.send({
130
- from: 'sender@example.com',
131
- to: 'recipient@example.com',
132
- cc: 'cc@example.com',
133
- bcc: ['bcc1@example.com', 'bcc2@example.com'],
134
- subject: 'Test Email',
135
- text: 'This is a test email',
136
- });
137
-
138
- expect(result.messageId).toBe('test-message-id');
139
- });
140
- });
141
-
142
- describe('sendTemplate', () => {
143
- it('should send email with React template', async () => {
144
- const result = await client.sendTemplate('test', {
145
- from: 'sender@example.com',
146
- to: 'recipient@example.com',
147
- subject: 'Test Template Email',
148
- props: { name: 'John' },
149
- });
150
-
151
- expect(result.messageId).toBe('test-message-id');
152
- });
153
-
154
- it('should send email with another template', async () => {
155
- const result = await client.sendTemplate('another', {
156
- from: 'sender@example.com',
157
- to: 'recipient@example.com',
158
- subject: 'Another Template Email',
159
- props: { title: 'Hello', message: 'World' },
160
- });
161
-
162
- expect(result.messageId).toBe('test-message-id');
163
- });
164
- });
165
-
166
- describe('verify', () => {
167
- it('should verify SMTP connection', async () => {
168
- const result = await client.verify();
169
- expect(result).toBe(true);
170
- });
171
- });
172
-
173
- describe('close', () => {
174
- it('should close SMTP connection', async () => {
175
- await expect(client.close()).resolves.not.toThrow();
176
- });
177
- });
178
-
179
- describe('template names', () => {
180
- it('should return available template names', () => {
181
- const templateNames = client.getTemplateNames();
182
- expect(templateNames).toEqual(['test', 'another']);
183
- });
184
- });
185
- });
package/src/client.ts DELETED
@@ -1,90 +0,0 @@
1
- import { render } from '@react-email/components';
2
- import nodemailer, { type Transporter } from 'nodemailer';
3
- import type {
4
- EmailClient,
5
- EmailClientConfig,
6
- SendOptions,
7
- SendResult,
8
- TemplateNames,
9
- TemplatePropsFor,
10
- TemplateRecord,
11
- } from './types';
12
-
13
- export class SMTPClient<T extends TemplateRecord> implements EmailClient<T> {
14
- private transporter: Transporter;
15
- private config: EmailClientConfig<T>;
16
-
17
- constructor(config: EmailClientConfig<T>) {
18
- this.config = config;
19
- this.transporter = nodemailer.createTransport(config.smtp as any);
20
- }
21
-
22
- async send(options: SendOptions): Promise<SendResult> {
23
- const mailOptions = {
24
- ...this.config.defaults,
25
- ...options,
26
- };
27
-
28
- if (!mailOptions.from) {
29
- throw new Error(
30
- 'The "from" field is required in email options or defaults',
31
- );
32
- }
33
-
34
- if (!mailOptions.text && !mailOptions.html) {
35
- throw new Error('Either text or html content must be provided');
36
- }
37
-
38
- const info = await this.transporter.sendMail(mailOptions);
39
-
40
- return {
41
- messageId: info.messageId,
42
- accepted: info.accepted || [],
43
- rejected: info.rejected || [],
44
- response: info.response,
45
- };
46
- }
47
-
48
- async sendTemplate<K extends TemplateNames<T>>(
49
- template: K,
50
- options: Omit<SendOptions, 'template'> & {
51
- props: TemplatePropsFor<T, K>;
52
- },
53
- ): Promise<SendResult> {
54
- const Component = this.config.templates[template];
55
- if (!Component) {
56
- throw new Error(`Template "${String(template)}" not found`);
57
- }
58
-
59
- const element = Component(options.props);
60
- const html = await render(element);
61
-
62
- return this.send({
63
- ...options,
64
- html,
65
- });
66
- }
67
-
68
- async verify(): Promise<boolean> {
69
- try {
70
- await this.transporter.verify();
71
- return true;
72
- } catch {
73
- return false;
74
- }
75
- }
76
-
77
- async close(): Promise<void> {
78
- this.transporter.close();
79
- }
80
-
81
- getTemplateNames(): TemplateNames<T>[] {
82
- return Object.keys(this.config.templates) as TemplateNames<T>[];
83
- }
84
- }
85
-
86
- export function createEmailClient<T extends TemplateRecord>(
87
- config: EmailClientConfig<T>,
88
- ): SMTPClient<T> {
89
- return new SMTPClient(config);
90
- }
package/src/index.ts DELETED
@@ -1,11 +0,0 @@
1
- export { createEmailClient, SMTPClient } from './client';
2
- export type {
3
- Attachment,
4
- EmailClient,
5
- EmailClientConfig,
6
- EmailOptions,
7
- EmailTemplate,
8
- PlainEmailOptions,
9
- SendResult,
10
- SMTPConfig,
11
- } from './types';