@lobehub/lobehub 2.0.0-next.124 → 2.0.0-next.126

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 (117) hide show
  1. package/.cursor/rules/db-migrations.mdc +16 -1
  2. package/.cursor/rules/project-introduce.mdc +1 -1
  3. package/.cursor/rules/project-structure.mdc +20 -2
  4. package/.env.example +148 -65
  5. package/.env.example.development +6 -8
  6. package/AGENTS.md +1 -3
  7. package/CHANGELOG.md +50 -0
  8. package/Dockerfile +7 -5
  9. package/GEMINI.md +63 -0
  10. package/changelog/v1.json +18 -0
  11. package/docs/development/database-schema.dbml +37 -0
  12. package/docs/self-hosting/advanced/auth.mdx +82 -2
  13. package/docs/self-hosting/advanced/auth.zh-CN.mdx +82 -2
  14. package/docs/self-hosting/environment-variables/auth.mdx +187 -1
  15. package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +187 -1
  16. package/locales/en-US/auth.json +93 -0
  17. package/locales/zh-CN/auth.json +107 -1
  18. package/package.json +5 -2
  19. package/packages/const/src/auth.ts +2 -1
  20. package/packages/database/migrations/0049_better_auth.sql +49 -0
  21. package/packages/database/migrations/meta/0048_snapshot.json +312 -932
  22. package/packages/database/migrations/meta/0049_snapshot.json +8151 -0
  23. package/packages/database/migrations/meta/_journal.json +8 -1
  24. package/packages/database/src/core/migrations.json +13 -0
  25. package/packages/database/src/index.ts +1 -0
  26. package/packages/database/src/models/__tests__/session.test.ts +1 -2
  27. package/packages/database/src/models/user.ts +9 -8
  28. package/packages/database/src/repositories/tableViewer/index.test.ts +2 -2
  29. package/packages/database/src/schemas/betterAuth.ts +63 -0
  30. package/packages/database/src/schemas/index.ts +1 -0
  31. package/packages/database/src/schemas/ragEvals.ts +1 -2
  32. package/packages/database/src/schemas/user.ts +3 -2
  33. package/packages/database/src/server/models/__tests__/user.test.ts +1 -4
  34. package/packages/types/src/user/preference.ts +11 -0
  35. package/packages/utils/src/server/__tests__/auth.test.ts +52 -0
  36. package/packages/utils/src/server/auth.ts +18 -1
  37. package/src/app/(backend)/api/auth/[...all]/route.ts +19 -0
  38. package/src/app/(backend)/api/auth/check-user/route.ts +62 -0
  39. package/src/app/(backend)/middleware/auth/index.ts +14 -0
  40. package/src/app/(backend)/middleware/auth/utils.test.ts +16 -0
  41. package/src/app/(backend)/middleware/auth/utils.ts +13 -10
  42. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +1 -0
  43. package/src/app/[variants]/(auth)/reset-password/layout.tsx +12 -0
  44. package/src/app/[variants]/(auth)/reset-password/page.tsx +209 -0
  45. package/src/app/[variants]/(auth)/signin/layout.tsx +12 -0
  46. package/src/app/[variants]/(auth)/signin/page.tsx +448 -0
  47. package/src/app/[variants]/(auth)/signup/[[...signup]]/BetterAuthSignUpForm.tsx +192 -0
  48. package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +31 -6
  49. package/src/app/[variants]/(auth)/verify-email/layout.tsx +12 -0
  50. package/src/app/[variants]/(auth)/verify-email/page.tsx +164 -0
  51. package/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +12 -10
  52. package/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/useCategory.test.tsx +13 -11
  53. package/src/app/[variants]/(main)/profile/(home)/Client.tsx +306 -52
  54. package/src/app/[variants]/(main)/profile/(home)/features/SSOProvidersList/index.tsx +89 -47
  55. package/src/auth.ts +118 -0
  56. package/src/components/NextAuth/AuthIcons.tsx +3 -1
  57. package/src/envs/auth.ts +260 -13
  58. package/src/envs/email.ts +37 -0
  59. package/src/features/User/UserPanel/PanelContent.tsx +6 -5
  60. package/src/features/User/__tests__/PanelContent.test.tsx +15 -6
  61. package/src/features/User/__tests__/UserAvatar.test.tsx +17 -6
  62. package/src/features/User/__tests__/useMenu.test.tsx +14 -12
  63. package/src/layout/AuthProvider/BetterAuth/UserUpdater.tsx +51 -0
  64. package/src/layout/AuthProvider/BetterAuth/index.tsx +14 -0
  65. package/src/layout/AuthProvider/index.tsx +3 -0
  66. package/src/libs/better-auth/auth-client.ts +34 -0
  67. package/src/libs/better-auth/constants.ts +13 -0
  68. package/src/libs/better-auth/email-templates/index.ts +3 -0
  69. package/src/libs/better-auth/email-templates/magic-link.ts +98 -0
  70. package/src/libs/better-auth/email-templates/reset-password.ts +91 -0
  71. package/src/libs/better-auth/email-templates/verification.ts +108 -0
  72. package/src/libs/better-auth/sso/helpers.ts +61 -0
  73. package/src/libs/better-auth/sso/index.ts +113 -0
  74. package/src/libs/better-auth/sso/providers/auth0.ts +33 -0
  75. package/src/libs/better-auth/sso/providers/authelia.ts +35 -0
  76. package/src/libs/better-auth/sso/providers/authentik.ts +35 -0
  77. package/src/libs/better-auth/sso/providers/casdoor.ts +48 -0
  78. package/src/libs/better-auth/sso/providers/cloudflare-zero-trust.ts +41 -0
  79. package/src/libs/better-auth/sso/providers/cognito.ts +45 -0
  80. package/src/libs/better-auth/sso/providers/feishu.ts +181 -0
  81. package/src/libs/better-auth/sso/providers/generic-oidc.ts +44 -0
  82. package/src/libs/better-auth/sso/providers/github.ts +30 -0
  83. package/src/libs/better-auth/sso/providers/google.ts +30 -0
  84. package/src/libs/better-auth/sso/providers/keycloak.ts +35 -0
  85. package/src/libs/better-auth/sso/providers/logto.ts +38 -0
  86. package/src/libs/better-auth/sso/providers/microsoft.ts +65 -0
  87. package/src/libs/better-auth/sso/providers/okta.ts +37 -0
  88. package/src/libs/better-auth/sso/providers/wechat.ts +140 -0
  89. package/src/libs/better-auth/sso/providers/zitadel.ts +54 -0
  90. package/src/libs/better-auth/sso/types.ts +25 -0
  91. package/src/libs/better-auth/utils/client.ts +1 -0
  92. package/src/libs/better-auth/utils/common.ts +20 -0
  93. package/src/libs/better-auth/utils/server.test.ts +61 -0
  94. package/src/libs/better-auth/utils/server.ts +18 -0
  95. package/src/libs/trpc/lambda/context.test.ts +116 -0
  96. package/src/libs/trpc/lambda/context.ts +27 -0
  97. package/src/libs/trpc/middleware/userAuth.ts +4 -2
  98. package/src/locales/default/auth.ts +114 -1
  99. package/src/proxy.ts +71 -7
  100. package/src/server/globalConfig/index.ts +12 -1
  101. package/src/server/routers/lambda/user.ts +4 -0
  102. package/src/server/services/email/README.md +241 -0
  103. package/src/server/services/email/impls/index.test.ts +39 -0
  104. package/src/server/services/email/impls/index.ts +32 -0
  105. package/src/server/services/email/impls/nodemailer/index.ts +108 -0
  106. package/src/server/services/email/impls/nodemailer/type.ts +31 -0
  107. package/src/server/services/email/impls/type.ts +61 -0
  108. package/src/server/services/email/index.test.ts +144 -0
  109. package/src/server/services/email/index.ts +40 -0
  110. package/src/services/user/index.test.ts +162 -2
  111. package/src/services/user/index.ts +6 -3
  112. package/src/store/user/slices/auth/action.test.ts +213 -16
  113. package/src/store/user/slices/auth/action.ts +86 -1
  114. package/src/store/user/slices/auth/initialState.ts +13 -2
  115. package/src/store/user/slices/auth/selectors.ts +6 -2
  116. package/src/store/user/slices/common/action.ts +5 -1
  117. package/src/app/(backend)/api/auth/[...nextauth]/route.ts +0 -3
@@ -0,0 +1,241 @@
1
+ # Email Service
2
+
3
+ A flexible email service implementation supporting multiple email providers.
4
+
5
+ ## Architecture
6
+
7
+ Based on the search service pattern, this service provides a unified interface for sending emails across different providers.
8
+
9
+ ```plaintext
10
+ EmailService
11
+ └── EmailServiceImpl (interface)
12
+ └── NodemailerImpl (SMTP provider)
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ### Basic Example
18
+
19
+ ```typescript
20
+ import { EmailService } from '@/server/services/email';
21
+
22
+ const emailService = new EmailService();
23
+
24
+ // Send a simple text email
25
+ await emailService.sendMail({
26
+ from: 'noreply@example.com',
27
+ to: 'user@example.com',
28
+ subject: 'Welcome to LobeChat',
29
+ text: 'Thanks for signing up!',
30
+ html: '<p>Thanks for signing up!</p>',
31
+ });
32
+ ```
33
+
34
+ ### With Multiple Recipients
35
+
36
+ ```typescript
37
+ await emailService.sendMail({
38
+ from: 'team@example.com',
39
+ to: ['user1@example.com', 'user2@example.com'],
40
+ subject: 'Team Update',
41
+ text: 'Check out our latest updates',
42
+ });
43
+ ```
44
+
45
+ ### With Attachments
46
+
47
+ ```typescript
48
+ await emailService.sendMail({
49
+ from: 'support@example.com',
50
+ to: 'user@example.com',
51
+ subject: 'Your Invoice',
52
+ text: 'Please find your invoice attached.',
53
+ attachments: [
54
+ {
55
+ filename: 'invoice.pdf',
56
+ path: '/path/to/invoice.pdf',
57
+ },
58
+ ],
59
+ });
60
+ ```
61
+
62
+ ### With Reply-To Address
63
+
64
+ ```typescript
65
+ await emailService.sendMail({
66
+ from: 'noreply@example.com',
67
+ replyTo: 'support@example.com',
68
+ to: 'user@example.com',
69
+ subject: 'Contact Us',
70
+ text: 'Reply to this email for support.',
71
+ });
72
+ ```
73
+
74
+ ## Configuration
75
+
76
+ ### Environment Variables
77
+
78
+ Configure SMTP settings using environment variables:
79
+
80
+ ```bash
81
+ # SMTP Server Configuration
82
+ SMTP_HOST=smtp.example.com
83
+ SMTP_PORT=587
84
+ SMTP_SECURE=false # true for port 465, false for other ports
85
+ SMTP_USER=your-username
86
+ SMTP_PASS=your-password
87
+ ```
88
+
89
+ ### Using Well-Known Services
90
+
91
+ You can also use well-known email services (Gmail, SendGrid, etc.):
92
+
93
+ ```typescript
94
+ import { EmailImplType, EmailService } from '@/server/services/email';
95
+ import { NodemailerImpl } from '@/server/services/email/impls/nodemailer';
96
+
97
+ const emailService = new EmailService(EmailImplType.Nodemailer);
98
+ // Configure in constructor with service name
99
+ ```
100
+
101
+ ### Testing with Ethereal
102
+
103
+ For development and testing, use [Ethereal Email](https://ethereal.email/):
104
+
105
+ ```typescript
106
+ // The preview URL will be logged automatically in development
107
+ const result = await emailService.sendMail({...});
108
+ console.log('Preview URL:', result.previewUrl);
109
+ ```
110
+
111
+ ## Verify Connection
112
+
113
+ Before sending emails, verify your SMTP configuration:
114
+
115
+ ```typescript
116
+ import { EmailService } from '@/server/services/email';
117
+
118
+ const emailService = new EmailService();
119
+
120
+ try {
121
+ await emailService.verify();
122
+ console.log('SMTP connection verified ✓');
123
+ } catch (error) {
124
+ console.error('SMTP verification failed:', error);
125
+ }
126
+ ```
127
+
128
+ ## Integration with Better-Auth
129
+
130
+ Example integration for email verification:
131
+
132
+ ```typescript
133
+ import { betterAuth } from 'better-auth';
134
+
135
+ import { EmailService } from '@/server/services/email';
136
+
137
+ export const auth = betterAuth({
138
+ emailAndPassword: {
139
+ enabled: true,
140
+ sendResetPasswordEmail: async ({ user, url }) => {
141
+ const emailService = new EmailService();
142
+
143
+ await emailService.sendMail({
144
+ from: 'noreply@lobechat.com',
145
+ to: user.email,
146
+ subject: 'Reset Your Password',
147
+ text: `Click here to reset your password: ${url}`,
148
+ html: `
149
+ <h1>Reset Your Password</h1>
150
+ <p>Click the link below to reset your password:</p>
151
+ <a href="${url}">Reset Password</a>
152
+ `,
153
+ });
154
+ },
155
+ },
156
+ emailVerification: {
157
+ enabled: true,
158
+ sendVerificationEmail: async ({ user, url }) => {
159
+ const emailService = new EmailService();
160
+
161
+ await emailService.sendMail({
162
+ from: 'noreply@lobechat.com',
163
+ to: user.email,
164
+ subject: 'Verify Your Email',
165
+ text: `Click here to verify your email: ${url}`,
166
+ html: `
167
+ <h1>Verify Your Email</h1>
168
+ <p>Click the link below to verify your email address:</p>
169
+ <a href="${url}">Verify Email</a>
170
+ `,
171
+ });
172
+ },
173
+ },
174
+ });
175
+ ```
176
+
177
+ ## Adding New Providers
178
+
179
+ To add a new email provider (e.g., Resend, SendGrid):
180
+
181
+ 1. Create provider implementation in `impls/[provider-name]/index.ts`:
182
+
183
+ ```typescript
184
+ import { EmailPayload, EmailResponse, EmailServiceImpl } from '../type';
185
+
186
+ export class ResendImpl implements EmailServiceImpl {
187
+ async sendMail(payload: EmailPayload): Promise<EmailResponse> {
188
+ // Implement using Resend API
189
+ }
190
+ }
191
+ ```
192
+
193
+ 2. Add to the enum in `impls/index.ts`:
194
+
195
+ ```typescript
196
+ export enum EmailImplType {
197
+ Nodemailer = 'nodemailer',
198
+ Resend = 'resend', // Add new provider
199
+ }
200
+ ```
201
+
202
+ 3. Update factory function in `impls/index.ts`:
203
+
204
+ ```typescript
205
+ export const createEmailServiceImpl = (type: EmailImplType) => {
206
+ switch (type) {
207
+ case EmailImplType.Nodemailer:
208
+ return new NodemailerImpl();
209
+ case EmailImplType.Resend:
210
+ return new ResendImpl();
211
+ default:
212
+ return new NodemailerImpl();
213
+ }
214
+ };
215
+ ```
216
+
217
+ ## Error Handling
218
+
219
+ The service throws `TRPCError` for various failure scenarios:
220
+
221
+ ```typescript
222
+ try {
223
+ await emailService.sendMail({...});
224
+ } catch (error) {
225
+ if (error.code === 'SERVICE_UNAVAILABLE') {
226
+ // Handle SMTP connection issues
227
+ } else if (error.code === 'PRECONDITION_FAILED') {
228
+ // Handle configuration errors
229
+ }
230
+ }
231
+ ```
232
+
233
+ ## Debugging
234
+
235
+ Enable debug logging:
236
+
237
+ ```bash
238
+ DEBUG=lobe-email:* node your-app.js
239
+ ```
240
+
241
+ This will log detailed information about email sending operations.
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { EmailImplType, createEmailServiceImpl } from './index';
4
+
5
+ vi.mock('./nodemailer', () => ({
6
+ NodemailerImpl: vi.fn().mockImplementation(() => ({
7
+ sendMail: vi.fn().mockResolvedValue({ messageId: 'test-id' }),
8
+ verify: vi.fn().mockResolvedValue(true),
9
+ })),
10
+ }));
11
+
12
+ describe('createEmailServiceImpl', () => {
13
+ it('should create NodemailerImpl by default', () => {
14
+ const impl = createEmailServiceImpl();
15
+
16
+ expect(impl).toBeDefined();
17
+ expect(impl.sendMail).toBeDefined();
18
+ });
19
+
20
+ it('should create NodemailerImpl when explicitly specified', () => {
21
+ const impl = createEmailServiceImpl(EmailImplType.Nodemailer);
22
+
23
+ expect(impl).toBeDefined();
24
+ expect(impl.sendMail).toBeDefined();
25
+ });
26
+
27
+ it('should fall back to NodemailerImpl for unknown type', () => {
28
+ const impl = createEmailServiceImpl('unknown' as EmailImplType);
29
+
30
+ expect(impl).toBeDefined();
31
+ expect(impl.sendMail).toBeDefined();
32
+ });
33
+ });
34
+
35
+ describe('EmailImplType enum', () => {
36
+ it('should have Nodemailer as a valid type', () => {
37
+ expect(EmailImplType.Nodemailer).toBe('nodemailer');
38
+ });
39
+ });
@@ -0,0 +1,32 @@
1
+ import { NodemailerImpl } from './nodemailer';
2
+ import { EmailServiceImpl } from './type';
3
+
4
+ /**
5
+ * Available email service implementations
6
+ */
7
+ export enum EmailImplType {
8
+ Nodemailer = 'nodemailer',
9
+ // Future providers can be added here:
10
+ // Resend = 'resend',
11
+ // SendGrid = 'sendgrid',
12
+ }
13
+
14
+ /**
15
+ * Create an email service implementation instance
16
+ */
17
+ export const createEmailServiceImpl = (
18
+ type: EmailImplType = EmailImplType.Nodemailer,
19
+ ): EmailServiceImpl => {
20
+ switch (type) {
21
+ case EmailImplType.Nodemailer: {
22
+ return new NodemailerImpl();
23
+ }
24
+
25
+ default: {
26
+ return new NodemailerImpl();
27
+ }
28
+ }
29
+ };
30
+
31
+ export type { EmailServiceImpl } from './type';
32
+ export type { EmailPayload, EmailResponse } from './type';
@@ -0,0 +1,108 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import debug from 'debug';
3
+ import nodemailer from 'nodemailer';
4
+ import type { Transporter } from 'nodemailer';
5
+
6
+ import { emailEnv } from '@/envs/email';
7
+
8
+ import { EmailPayload, EmailResponse, EmailServiceImpl } from '../type';
9
+ import { NodemailerConfig } from './type';
10
+
11
+ const log = debug('lobe-email:Nodemailer');
12
+
13
+ /**
14
+ * Nodemailer implementation of the email service
15
+ */
16
+ export class NodemailerImpl implements EmailServiceImpl {
17
+ private transporter: Transporter;
18
+
19
+ constructor() {
20
+ log('Initializing Nodemailer from environment variables');
21
+
22
+ if (!emailEnv.SMTP_USER || !emailEnv.SMTP_PASS) {
23
+ throw new Error(
24
+ 'SMTP_USER and SMTP_PASS environment variables are required to use email service. Please configure SMTP settings in your .env file.',
25
+ );
26
+ }
27
+
28
+ const transportConfig: NodemailerConfig = {
29
+ auth: {
30
+ pass: emailEnv.SMTP_PASS,
31
+ user: emailEnv.SMTP_USER,
32
+ },
33
+ host: emailEnv.SMTP_HOST ?? 'localhost',
34
+ port: emailEnv.SMTP_PORT ?? 587,
35
+ secure: emailEnv.SMTP_SECURE ?? false,
36
+ };
37
+
38
+ try {
39
+ this.transporter = nodemailer.createTransport(transportConfig);
40
+ log('Nodemailer transporter created successfully');
41
+ } catch (error) {
42
+ log.extend('error')('Failed to create Nodemailer transporter: %o', error);
43
+ throw new TRPCError({
44
+ cause: error,
45
+ code: 'INTERNAL_SERVER_ERROR',
46
+ message: 'Failed to initialize Nodemailer transport',
47
+ });
48
+ }
49
+ }
50
+
51
+ async sendMail(payload: EmailPayload): Promise<EmailResponse> {
52
+ // Use SMTP_USER as default sender if not provided
53
+ const from = payload.from ?? emailEnv.SMTP_USER!;
54
+
55
+ log('Sending email with payload: %o', {
56
+ from,
57
+ subject: payload.subject,
58
+ to: payload.to,
59
+ });
60
+
61
+ try {
62
+ const info = await this.transporter.sendMail({
63
+ attachments: payload.attachments,
64
+ from,
65
+ html: payload.html,
66
+ replyTo: payload.replyTo,
67
+ subject: payload.subject,
68
+ text: payload.text,
69
+ to: payload.to,
70
+ });
71
+
72
+ log('Email sent successfully with message ID: %s', info.messageId);
73
+
74
+ const previewUrl = nodemailer.getTestMessageUrl(info);
75
+
76
+ return {
77
+ messageId: info.messageId,
78
+ previewUrl: previewUrl || undefined,
79
+ };
80
+ } catch (error) {
81
+ log.extend('error')('Failed to send email: %o', error);
82
+ throw new TRPCError({
83
+ cause: error,
84
+ code: 'SERVICE_UNAVAILABLE',
85
+ message: `Failed to send email: ${(error as Error).message}`,
86
+ });
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Verify the SMTP connection configuration
92
+ */
93
+ async verify(): Promise<boolean> {
94
+ try {
95
+ log('Verifying SMTP connection...');
96
+ await this.transporter.verify();
97
+ log('SMTP connection verified successfully');
98
+ return true;
99
+ } catch (error) {
100
+ log.extend('error')('SMTP verification failed: %o', error);
101
+ throw new TRPCError({
102
+ cause: error,
103
+ code: 'SERVICE_UNAVAILABLE',
104
+ message: 'Failed to verify SMTP connection',
105
+ });
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Nodemailer SMTP transport configuration
3
+ */
4
+ export interface NodemailerConfig {
5
+ /**
6
+ * Authentication credentials
7
+ */
8
+ auth?: {
9
+ pass: string;
10
+ user: string;
11
+ };
12
+ /**
13
+ * SMTP server hostname
14
+ */
15
+ host?: string;
16
+ /**
17
+ * SMTP server port
18
+ * @default 587
19
+ */
20
+ port?: number;
21
+ /**
22
+ * Use TLS connection
23
+ * @default false
24
+ */
25
+ secure?: boolean;
26
+ /**
27
+ * Well-known service name (e.g., 'Gmail', 'SendGrid')
28
+ * When set, overrides host, port, and secure
29
+ */
30
+ service?: string;
31
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Email message payload
3
+ */
4
+ export interface EmailPayload {
5
+ /**
6
+ * Email attachments
7
+ */
8
+ attachments?: Array<{
9
+ content?: Buffer | string;
10
+ filename?: string;
11
+ path?: string;
12
+ }>;
13
+ /**
14
+ * Sender address (defaults to SMTP_USER if not provided)
15
+ */
16
+ from?: string;
17
+ /**
18
+ * HTML body of the email
19
+ */
20
+ html?: string;
21
+ /**
22
+ * Reply-To address
23
+ */
24
+ replyTo?: string;
25
+ /**
26
+ * Subject line
27
+ */
28
+ subject: string;
29
+ /**
30
+ * Plain text body of the email
31
+ */
32
+ text?: string;
33
+ /**
34
+ * Recipient address(es)
35
+ */
36
+ to: string | string[];
37
+ }
38
+
39
+ /**
40
+ * Email send response
41
+ */
42
+ export interface EmailResponse {
43
+ /**
44
+ * Message ID assigned by the email service
45
+ */
46
+ messageId: string;
47
+ /**
48
+ * Preview URL for test emails (e.g., Ethereal)
49
+ */
50
+ previewUrl?: string;
51
+ }
52
+
53
+ /**
54
+ * Email service implementation interface
55
+ */
56
+ export interface EmailServiceImpl {
57
+ /**
58
+ * Send an email
59
+ */
60
+ sendMail(payload: EmailPayload): Promise<EmailResponse>;
61
+ }
@@ -0,0 +1,144 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { EmailImplType, createEmailServiceImpl } from './impls';
4
+ import { EmailService } from './index';
5
+
6
+ // Mock dependencies
7
+ vi.mock('./impls');
8
+
9
+ describe('EmailService', () => {
10
+ let emailService: EmailService;
11
+ let mockEmailImpl: ReturnType<typeof createMockEmailImpl>;
12
+
13
+ function createMockEmailImpl() {
14
+ return {
15
+ sendMail: vi.fn(),
16
+ verify: vi.fn(),
17
+ };
18
+ }
19
+
20
+ beforeEach(() => {
21
+ vi.clearAllMocks();
22
+ mockEmailImpl = createMockEmailImpl();
23
+ vi.mocked(createEmailServiceImpl).mockReturnValue(mockEmailImpl as any);
24
+ emailService = new EmailService();
25
+ });
26
+
27
+ describe('constructor', () => {
28
+ it('should create instance with default email implementation', () => {
29
+ expect(createEmailServiceImpl).toHaveBeenCalledWith(undefined);
30
+ });
31
+
32
+ it('should create instance with specified implementation type', () => {
33
+ emailService = new EmailService(EmailImplType.Nodemailer);
34
+ expect(createEmailServiceImpl).toHaveBeenCalledWith(EmailImplType.Nodemailer);
35
+ });
36
+ });
37
+
38
+ describe('sendMail', () => {
39
+ it('should call emailImpl.sendMail with correct payload', async () => {
40
+ const mockResponse = {
41
+ messageId: 'test-message-id',
42
+ previewUrl: 'https://ethereal.email/message/xxx',
43
+ };
44
+ mockEmailImpl.sendMail.mockResolvedValue(mockResponse);
45
+
46
+ const payload = {
47
+ from: 'sender@example.com',
48
+ html: '<p>Hello world</p>',
49
+ subject: 'Test Email',
50
+ text: 'Hello world',
51
+ to: 'recipient@example.com',
52
+ };
53
+
54
+ const result = await emailService.sendMail(payload);
55
+
56
+ expect(mockEmailImpl.sendMail).toHaveBeenCalledWith(payload);
57
+ expect(result).toBe(mockResponse);
58
+ });
59
+
60
+ it('should support multiple recipients', async () => {
61
+ const mockResponse = {
62
+ messageId: 'test-message-id',
63
+ };
64
+ mockEmailImpl.sendMail.mockResolvedValue(mockResponse);
65
+
66
+ const payload = {
67
+ from: 'sender@example.com',
68
+ subject: 'Test Email',
69
+ text: 'Hello world',
70
+ to: ['recipient1@example.com', 'recipient2@example.com'],
71
+ };
72
+
73
+ await emailService.sendMail(payload);
74
+
75
+ expect(mockEmailImpl.sendMail).toHaveBeenCalledWith(payload);
76
+ });
77
+
78
+ it('should support attachments', async () => {
79
+ const mockResponse = {
80
+ messageId: 'test-message-id',
81
+ };
82
+ mockEmailImpl.sendMail.mockResolvedValue(mockResponse);
83
+
84
+ const payload = {
85
+ attachments: [
86
+ {
87
+ content: Buffer.from('test content'),
88
+ filename: 'test.txt',
89
+ },
90
+ ],
91
+ from: 'sender@example.com',
92
+ subject: 'Test Email',
93
+ text: 'Hello world',
94
+ to: 'recipient@example.com',
95
+ };
96
+
97
+ await emailService.sendMail(payload);
98
+
99
+ expect(mockEmailImpl.sendMail).toHaveBeenCalledWith(payload);
100
+ });
101
+
102
+ it('should support reply-to address', async () => {
103
+ const mockResponse = {
104
+ messageId: 'test-message-id',
105
+ };
106
+ mockEmailImpl.sendMail.mockResolvedValue(mockResponse);
107
+
108
+ const payload = {
109
+ from: 'noreply@example.com',
110
+ replyTo: 'support@example.com',
111
+ subject: 'Test Email',
112
+ text: 'Hello world',
113
+ to: 'recipient@example.com',
114
+ };
115
+
116
+ await emailService.sendMail(payload);
117
+
118
+ expect(mockEmailImpl.sendMail).toHaveBeenCalledWith(payload);
119
+ });
120
+ });
121
+
122
+ describe('verify', () => {
123
+ it('should call emailImpl.verify if available', async () => {
124
+ mockEmailImpl.verify.mockResolvedValue(true);
125
+
126
+ const result = await emailService.verify();
127
+
128
+ expect(mockEmailImpl.verify).toHaveBeenCalled();
129
+ expect(result).toBe(true);
130
+ });
131
+
132
+ it('should return true if verify method is not available', async () => {
133
+ const mockImplWithoutVerify = {
134
+ sendMail: vi.fn(),
135
+ };
136
+ vi.mocked(createEmailServiceImpl).mockReturnValue(mockImplWithoutVerify as any);
137
+ emailService = new EmailService();
138
+
139
+ const result = await emailService.verify();
140
+
141
+ expect(result).toBe(true);
142
+ });
143
+ });
144
+ });
@@ -0,0 +1,40 @@
1
+
2
+ import { EmailImplType, EmailPayload, EmailResponse, createEmailServiceImpl } from './impls';
3
+ import type { EmailServiceImpl } from './impls';
4
+
5
+ /**
6
+ * Email service class
7
+ * Provides email sending functionality with multiple provider support
8
+ */
9
+ export class EmailService {
10
+ private emailImpl: EmailServiceImpl;
11
+
12
+ constructor(implType?: EmailImplType) {
13
+ this.emailImpl = createEmailServiceImpl(implType);
14
+ }
15
+
16
+ /**
17
+ * Send an email
18
+ */
19
+ async sendMail(payload: EmailPayload): Promise<EmailResponse> {
20
+ return this.emailImpl.sendMail(payload);
21
+ }
22
+
23
+ /**
24
+ * Verify the email service configuration
25
+ * Note: Only available for Nodemailer implementation
26
+ */
27
+ async verify(): Promise<boolean> {
28
+ // Check if the implementation has a verify method
29
+ if ('verify' in this.emailImpl && typeof this.emailImpl.verify === 'function') {
30
+ return this.emailImpl.verify();
31
+ }
32
+
33
+ // For implementations without verify, assume it's valid
34
+ return true;
35
+ }
36
+ }
37
+
38
+ // Export types
39
+ export type { EmailPayload, EmailResponse } from './impls';
40
+ export { EmailImplType } from './impls';