@geekmidas/emailkit 0.0.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.
@@ -0,0 +1,176 @@
1
+ const require_chunk = require('./chunk-CUT6urMc.cjs');
2
+ const react_jsx_runtime = require_chunk.__toESM(require("react/jsx-runtime"));
3
+
4
+ //#region src/templates.tsx
5
+ const emailStyles = {
6
+ container: {
7
+ fontFamily: "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif",
8
+ lineHeight: 1.6,
9
+ color: "#333",
10
+ maxWidth: "600px",
11
+ margin: "0 auto",
12
+ padding: "20px"
13
+ },
14
+ heading: {
15
+ color: "#2c3e50",
16
+ marginBottom: "20px"
17
+ },
18
+ button: {
19
+ display: "inline-block",
20
+ padding: "12px 24px",
21
+ backgroundColor: "#3498db",
22
+ color: "#ffffff",
23
+ textDecoration: "none",
24
+ borderRadius: "4px",
25
+ fontWeight: "bold"
26
+ },
27
+ footer: {
28
+ marginTop: "40px",
29
+ paddingTop: "20px",
30
+ borderTop: "1px solid #eee",
31
+ fontSize: "14px",
32
+ color: "#666"
33
+ }
34
+ };
35
+ function EmailLayout({ children, preheader, title }) {
36
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("html", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("head", { children: [
37
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("meta", { charSet: "utf-8" }),
38
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("meta", {
39
+ name: "viewport",
40
+ content: "width=device-width, initial-scale=1.0"
41
+ }),
42
+ title && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("title", { children: title }),
43
+ preheader && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
44
+ style: {
45
+ display: "none",
46
+ maxHeight: 0,
47
+ overflow: "hidden"
48
+ },
49
+ children: preheader
50
+ })
51
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("body", {
52
+ style: {
53
+ margin: 0,
54
+ padding: 0,
55
+ backgroundColor: "#f4f4f4"
56
+ },
57
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58
+ style: emailStyles.container,
59
+ children
60
+ })
61
+ })] });
62
+ }
63
+ function WelcomeEmail({ name, confirmationUrl }) {
64
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(EmailLayout, {
65
+ title: "Welcome!",
66
+ preheader: `Welcome to our service, ${name}!`,
67
+ children: [
68
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h1", {
69
+ style: emailStyles.heading,
70
+ children: [
71
+ "Welcome, ",
72
+ name,
73
+ "!"
74
+ ]
75
+ }),
76
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "We're excited to have you on board. Your account has been successfully created." }),
77
+ confirmationUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "Please confirm your email address by clicking the button below:" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
78
+ style: {
79
+ textAlign: "center",
80
+ margin: "30px 0"
81
+ },
82
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
83
+ href: confirmationUrl,
84
+ style: emailStyles.button,
85
+ children: "Confirm Email"
86
+ })
87
+ })] }),
88
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
89
+ style: emailStyles.footer,
90
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "If you have any questions, feel free to reply to this email." }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
91
+ "Best regards,",
92
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}),
93
+ "The Team"
94
+ ] })]
95
+ })
96
+ ]
97
+ });
98
+ }
99
+ function PasswordResetEmail({ name, resetUrl, expiresIn }) {
100
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(EmailLayout, {
101
+ title: "Password Reset Request",
102
+ preheader: "Reset your password",
103
+ children: [
104
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
105
+ style: emailStyles.heading,
106
+ children: "Password Reset Request"
107
+ }),
108
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
109
+ "Hi ",
110
+ name,
111
+ ","
112
+ ] }),
113
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "We received a request to reset your password. Click the button below to create a new password:" }),
114
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
115
+ style: {
116
+ textAlign: "center",
117
+ margin: "30px 0"
118
+ },
119
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
120
+ href: resetUrl,
121
+ style: emailStyles.button,
122
+ children: "Reset Password"
123
+ })
124
+ }),
125
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
126
+ "This link will expire in ",
127
+ expiresIn,
128
+ ". If you didn't request a password reset, you can safely ignore this email."
129
+ ] }),
130
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
131
+ style: emailStyles.footer,
132
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "For security reasons, this link can only be used once." })
133
+ })
134
+ ]
135
+ });
136
+ }
137
+ function NotificationEmail({ name, title, message, actionUrl, actionText = "View Details" }) {
138
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(EmailLayout, {
139
+ title,
140
+ preheader: message,
141
+ children: [
142
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
143
+ style: emailStyles.heading,
144
+ children: title
145
+ }),
146
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
147
+ "Hi ",
148
+ name,
149
+ ","
150
+ ] }),
151
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: message }),
152
+ actionUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
153
+ style: {
154
+ textAlign: "center",
155
+ margin: "30px 0"
156
+ },
157
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
158
+ href: actionUrl,
159
+ style: emailStyles.button,
160
+ children: actionText
161
+ })
162
+ }),
163
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
164
+ style: emailStyles.footer,
165
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "This is an automated notification from our system." })
166
+ })
167
+ ]
168
+ });
169
+ }
170
+
171
+ //#endregion
172
+ exports.EmailLayout = EmailLayout;
173
+ exports.NotificationEmail = NotificationEmail;
174
+ exports.PasswordResetEmail = PasswordResetEmail;
175
+ exports.WelcomeEmail = WelcomeEmail;
176
+ exports.emailStyles = emailStyles;
@@ -0,0 +1,171 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+
3
+ //#region src/templates.tsx
4
+ const emailStyles = {
5
+ container: {
6
+ fontFamily: "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif",
7
+ lineHeight: 1.6,
8
+ color: "#333",
9
+ maxWidth: "600px",
10
+ margin: "0 auto",
11
+ padding: "20px"
12
+ },
13
+ heading: {
14
+ color: "#2c3e50",
15
+ marginBottom: "20px"
16
+ },
17
+ button: {
18
+ display: "inline-block",
19
+ padding: "12px 24px",
20
+ backgroundColor: "#3498db",
21
+ color: "#ffffff",
22
+ textDecoration: "none",
23
+ borderRadius: "4px",
24
+ fontWeight: "bold"
25
+ },
26
+ footer: {
27
+ marginTop: "40px",
28
+ paddingTop: "20px",
29
+ borderTop: "1px solid #eee",
30
+ fontSize: "14px",
31
+ color: "#666"
32
+ }
33
+ };
34
+ function EmailLayout({ children, preheader, title }) {
35
+ return /* @__PURE__ */ jsxs("html", { children: [/* @__PURE__ */ jsxs("head", { children: [
36
+ /* @__PURE__ */ jsx("meta", { charSet: "utf-8" }),
37
+ /* @__PURE__ */ jsx("meta", {
38
+ name: "viewport",
39
+ content: "width=device-width, initial-scale=1.0"
40
+ }),
41
+ title && /* @__PURE__ */ jsx("title", { children: title }),
42
+ preheader && /* @__PURE__ */ jsx("div", {
43
+ style: {
44
+ display: "none",
45
+ maxHeight: 0,
46
+ overflow: "hidden"
47
+ },
48
+ children: preheader
49
+ })
50
+ ] }), /* @__PURE__ */ jsx("body", {
51
+ style: {
52
+ margin: 0,
53
+ padding: 0,
54
+ backgroundColor: "#f4f4f4"
55
+ },
56
+ children: /* @__PURE__ */ jsx("div", {
57
+ style: emailStyles.container,
58
+ children
59
+ })
60
+ })] });
61
+ }
62
+ function WelcomeEmail({ name, confirmationUrl }) {
63
+ return /* @__PURE__ */ jsxs(EmailLayout, {
64
+ title: "Welcome!",
65
+ preheader: `Welcome to our service, ${name}!`,
66
+ children: [
67
+ /* @__PURE__ */ jsxs("h1", {
68
+ style: emailStyles.heading,
69
+ children: [
70
+ "Welcome, ",
71
+ name,
72
+ "!"
73
+ ]
74
+ }),
75
+ /* @__PURE__ */ jsx("p", { children: "We're excited to have you on board. Your account has been successfully created." }),
76
+ confirmationUrl && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("p", { children: "Please confirm your email address by clicking the button below:" }), /* @__PURE__ */ jsx("p", {
77
+ style: {
78
+ textAlign: "center",
79
+ margin: "30px 0"
80
+ },
81
+ children: /* @__PURE__ */ jsx("a", {
82
+ href: confirmationUrl,
83
+ style: emailStyles.button,
84
+ children: "Confirm Email"
85
+ })
86
+ })] }),
87
+ /* @__PURE__ */ jsxs("div", {
88
+ style: emailStyles.footer,
89
+ children: [/* @__PURE__ */ jsx("p", { children: "If you have any questions, feel free to reply to this email." }), /* @__PURE__ */ jsxs("p", { children: [
90
+ "Best regards,",
91
+ /* @__PURE__ */ jsx("br", {}),
92
+ "The Team"
93
+ ] })]
94
+ })
95
+ ]
96
+ });
97
+ }
98
+ function PasswordResetEmail({ name, resetUrl, expiresIn }) {
99
+ return /* @__PURE__ */ jsxs(EmailLayout, {
100
+ title: "Password Reset Request",
101
+ preheader: "Reset your password",
102
+ children: [
103
+ /* @__PURE__ */ jsx("h1", {
104
+ style: emailStyles.heading,
105
+ children: "Password Reset Request"
106
+ }),
107
+ /* @__PURE__ */ jsxs("p", { children: [
108
+ "Hi ",
109
+ name,
110
+ ","
111
+ ] }),
112
+ /* @__PURE__ */ jsx("p", { children: "We received a request to reset your password. Click the button below to create a new password:" }),
113
+ /* @__PURE__ */ jsx("p", {
114
+ style: {
115
+ textAlign: "center",
116
+ margin: "30px 0"
117
+ },
118
+ children: /* @__PURE__ */ jsx("a", {
119
+ href: resetUrl,
120
+ style: emailStyles.button,
121
+ children: "Reset Password"
122
+ })
123
+ }),
124
+ /* @__PURE__ */ jsxs("p", { children: [
125
+ "This link will expire in ",
126
+ expiresIn,
127
+ ". If you didn't request a password reset, you can safely ignore this email."
128
+ ] }),
129
+ /* @__PURE__ */ jsx("div", {
130
+ style: emailStyles.footer,
131
+ children: /* @__PURE__ */ jsx("p", { children: "For security reasons, this link can only be used once." })
132
+ })
133
+ ]
134
+ });
135
+ }
136
+ function NotificationEmail({ name, title, message, actionUrl, actionText = "View Details" }) {
137
+ return /* @__PURE__ */ jsxs(EmailLayout, {
138
+ title,
139
+ preheader: message,
140
+ children: [
141
+ /* @__PURE__ */ jsx("h1", {
142
+ style: emailStyles.heading,
143
+ children: title
144
+ }),
145
+ /* @__PURE__ */ jsxs("p", { children: [
146
+ "Hi ",
147
+ name,
148
+ ","
149
+ ] }),
150
+ /* @__PURE__ */ jsx("p", { children: message }),
151
+ actionUrl && /* @__PURE__ */ jsx("p", {
152
+ style: {
153
+ textAlign: "center",
154
+ margin: "30px 0"
155
+ },
156
+ children: /* @__PURE__ */ jsx("a", {
157
+ href: actionUrl,
158
+ style: emailStyles.button,
159
+ children: actionText
160
+ })
161
+ }),
162
+ /* @__PURE__ */ jsx("div", {
163
+ style: emailStyles.footer,
164
+ children: /* @__PURE__ */ jsx("p", { children: "This is an automated notification from our system." })
165
+ })
166
+ ]
167
+ });
168
+ }
169
+
170
+ //#endregion
171
+ export { EmailLayout, NotificationEmail, PasswordResetEmail, WelcomeEmail, emailStyles };
package/dist/types.cjs ADDED
File without changes
package/dist/types.mjs ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@geekmidas/emailkit",
3
+ "version": "0.0.1",
4
+ "description": "Type-safe email client with SMTP support and React templates",
5
+ "private": false,
6
+ "type": "module",
7
+ "exports": {
8
+ "./*": {
9
+ "import": "./dist/*.mjs",
10
+ "require": "./dist/*.cjs",
11
+ "types": "./src/*.ts"
12
+ }
13
+ },
14
+ "publishConfig": {
15
+ "registry": "https://registry.npmjs.org/",
16
+ "access": "public"
17
+ },
18
+ "dependencies": {
19
+ "nodemailer": "^6.9.7",
20
+ "react": "~19.1.0",
21
+ "react-dom": "~19.1.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.0.0",
25
+ "@types/nodemailer": "^6.4.14",
26
+ "@types/react": "^18.2.0",
27
+ "@types/react-dom": "^18.2.0",
28
+ "tsdown": "^0.2.17",
29
+ "typescript": "^5.8.2",
30
+ "vitest": "^2.1.8"
31
+ },
32
+ "peerDependencies": {
33
+ "react": ">=16.8.0",
34
+ "react-dom": ">=16.8.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=22.0.0"
38
+ }
39
+ }
@@ -0,0 +1,182 @@
1
+ // biome-ignore lint/correctness/noUnusedImports: <explanation>
2
+ import React from 'react';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { type SMTPClient, createEmailClient } 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
+ }: { title: string; message: string }) => (
29
+ <div>
30
+ <h1>{title}</h1>
31
+ <p>{message}</p>
32
+ </div>
33
+ );
34
+
35
+ describe('SMTPClient', () => {
36
+ let client: SMTPClient<typeof templates>;
37
+
38
+ const templates = {
39
+ test: TestTemplate,
40
+ another: AnotherTemplate,
41
+ };
42
+
43
+ const config: EmailClientConfig<typeof templates> = {
44
+ smtp: {
45
+ host: 'smtp.example.com',
46
+ port: 587,
47
+ auth: {
48
+ user: 'test@example.com',
49
+ pass: 'password',
50
+ },
51
+ },
52
+ templates,
53
+ defaults: {
54
+ from: 'noreply@example.com',
55
+ },
56
+ };
57
+
58
+ beforeEach(() => {
59
+ client = createEmailClient(config);
60
+ });
61
+
62
+ describe('send', () => {
63
+ it('should send plain text email', async () => {
64
+ const result = await client.send({
65
+ from: 'sender@example.com',
66
+ to: 'recipient@example.com',
67
+ subject: 'Test Email',
68
+ text: 'This is a test email',
69
+ });
70
+
71
+ expect(result).toEqual({
72
+ messageId: 'test-message-id',
73
+ accepted: ['test@example.com'],
74
+ rejected: [],
75
+ response: '250 OK',
76
+ });
77
+ });
78
+
79
+ it('should send HTML email', async () => {
80
+ const result = await client.send({
81
+ from: 'sender@example.com',
82
+ to: 'recipient@example.com',
83
+ subject: 'Test Email',
84
+ html: '<p>This is a test email</p>',
85
+ });
86
+
87
+ expect(result.messageId).toBe('test-message-id');
88
+ });
89
+
90
+ it('should use default from address', async () => {
91
+ const result = await client.send({
92
+ to: 'recipient@example.com',
93
+ subject: 'Test Email',
94
+ text: 'This is a test email',
95
+ from: {
96
+ name: 'Test Sender',
97
+ address: 'sender@example.com',
98
+ },
99
+ });
100
+
101
+ expect(result.messageId).toBe('test-message-id');
102
+ });
103
+
104
+ it('should throw error if no content provided', async () => {
105
+ await expect(
106
+ client.send({
107
+ from: 'sender@example.com',
108
+ to: 'recipient@example.com',
109
+ subject: 'Test Email',
110
+ }),
111
+ ).rejects.toThrow('Either text or html content must be provided');
112
+ });
113
+
114
+ it('should support multiple recipients', async () => {
115
+ const result = await client.send({
116
+ from: 'sender@example.com',
117
+ to: ['recipient1@example.com', 'recipient2@example.com'],
118
+ subject: 'Test Email',
119
+ text: 'This is a test email',
120
+ });
121
+
122
+ expect(result.messageId).toBe('test-message-id');
123
+ });
124
+
125
+ it('should support CC and BCC', async () => {
126
+ const result = await client.send({
127
+ from: 'sender@example.com',
128
+ to: 'recipient@example.com',
129
+ cc: 'cc@example.com',
130
+ bcc: ['bcc1@example.com', 'bcc2@example.com'],
131
+ subject: 'Test Email',
132
+ text: 'This is a test email',
133
+ });
134
+
135
+ expect(result.messageId).toBe('test-message-id');
136
+ });
137
+ });
138
+
139
+ describe('sendTemplate', () => {
140
+ it('should send email with React template', async () => {
141
+ const result = await client.sendTemplate('test', {
142
+ from: 'sender@example.com',
143
+ to: 'recipient@example.com',
144
+ subject: 'Test Template Email',
145
+ props: { name: 'John' },
146
+ });
147
+
148
+ expect(result.messageId).toBe('test-message-id');
149
+ });
150
+
151
+ it('should send email with another template', async () => {
152
+ const result = await client.sendTemplate('another', {
153
+ from: 'sender@example.com',
154
+ to: 'recipient@example.com',
155
+ subject: 'Another Template Email',
156
+ props: { title: 'Hello', message: 'World' },
157
+ });
158
+
159
+ expect(result.messageId).toBe('test-message-id');
160
+ });
161
+ });
162
+
163
+ describe('verify', () => {
164
+ it('should verify SMTP connection', async () => {
165
+ const result = await client.verify();
166
+ expect(result).toBe(true);
167
+ });
168
+ });
169
+
170
+ describe('close', () => {
171
+ it('should close SMTP connection', async () => {
172
+ await expect(client.close()).resolves.not.toThrow();
173
+ });
174
+ });
175
+
176
+ describe('template names', () => {
177
+ it('should return available template names', () => {
178
+ const templateNames = client.getTemplateNames();
179
+ expect(templateNames).toEqual(['test', 'another']);
180
+ });
181
+ });
182
+ });
package/src/client.ts ADDED
@@ -0,0 +1,87 @@
1
+ import nodemailer, { type Transporter } from 'nodemailer';
2
+ import { renderToStaticMarkup } from 'react-dom/server';
3
+ import type {
4
+ EmailClient,
5
+ EmailClientConfig,
6
+ EmailOptions,
7
+ PlainEmailOptions,
8
+ SendResult,
9
+ TemplateNames,
10
+ TemplatePropsFor,
11
+ TemplateRecord,
12
+ } from './types';
13
+
14
+ export class SMTPClient<T extends TemplateRecord>
15
+ implements EmailClient<T>
16
+ {
17
+ private transporter: Transporter;
18
+ private config: EmailClientConfig<T>;
19
+
20
+ constructor(config: EmailClientConfig<T>) {
21
+ this.config = config;
22
+ this.transporter = nodemailer.createTransport(config.smtp as any);
23
+ }
24
+
25
+ async send(options: PlainEmailOptions): Promise<SendResult> {
26
+ const mailOptions = {
27
+ ...this.config.defaults,
28
+ ...options,
29
+ };
30
+
31
+ if (!mailOptions.text && !mailOptions.html) {
32
+ throw new Error('Either text or html content must be provided');
33
+ }
34
+
35
+ const info = await this.transporter.sendMail(mailOptions);
36
+
37
+ return {
38
+ messageId: info.messageId,
39
+ accepted: info.accepted || [],
40
+ rejected: info.rejected || [],
41
+ response: info.response,
42
+ };
43
+ }
44
+
45
+ async sendTemplate<K extends TemplateNames<T>>(
46
+ template: K,
47
+ options: Omit<EmailOptions, 'template'> & {
48
+ props: TemplatePropsFor<T, K>;
49
+ },
50
+ ): Promise<SendResult> {
51
+ const templateFn = this.config.templates[template];
52
+ if (!templateFn) {
53
+ throw new Error(`Template "${String(template)}" not found`);
54
+ }
55
+
56
+ const element = templateFn(options.props);
57
+ const html = renderToStaticMarkup(element);
58
+
59
+ return this.send({
60
+ ...options,
61
+ html,
62
+ });
63
+ }
64
+
65
+ async verify(): Promise<boolean> {
66
+ try {
67
+ await this.transporter.verify();
68
+ return true;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ async close(): Promise<void> {
75
+ this.transporter.close();
76
+ }
77
+
78
+ getTemplateNames(): TemplateNames<T>[] {
79
+ return Object.keys(this.config.templates) as TemplateNames<T>[];
80
+ }
81
+ }
82
+
83
+ export function createEmailClient<T extends TemplateRecord>(
84
+ config: EmailClientConfig<T>,
85
+ ): SMTPClient<T> {
86
+ return new SMTPClient(config);
87
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { SMTPClient, createEmailClient } from './client';
2
+ export type {
3
+ EmailClient,
4
+ EmailClientConfig,
5
+ SMTPConfig,
6
+ EmailOptions,
7
+ PlainEmailOptions,
8
+ SendResult,
9
+ Attachment,
10
+ EmailTemplate,
11
+ } from './types';