@rx-ted/packages-honest-plugins 1.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.
- package/CHANGELOG.md +13 -0
- package/README.md +45 -0
- package/api-doc/.turbo/turbo-build.log +1 -0
- package/api-doc/.turbo/turbo-test.log +11 -0
- package/api-doc/.turbo/turbo-typecheck.log +1 -0
- package/api-doc/CHANGELOG.md +22 -0
- package/api-doc/README.md +80 -0
- package/api-doc/dist/api-doc.plugin.d.ts +53 -0
- package/api-doc/dist/api-doc.plugin.js +452 -0
- package/api-doc/dist/index.d.ts +5 -0
- package/api-doc/dist/index.js +3 -0
- package/api-doc/dist/interfaces/renderer.interface.d.ts +7 -0
- package/api-doc/dist/interfaces/renderer.interface.js +1 -0
- package/api-doc/dist/renderers/scalar.renderer.d.ts +10 -0
- package/api-doc/dist/renderers/scalar.renderer.js +76 -0
- package/api-doc/dist/renderers/swagger.renderer.d.ts +10 -0
- package/api-doc/dist/renderers/swagger.renderer.js +83 -0
- package/api-doc/node_modules/.bin/esbuild +22 -0
- package/api-doc/node_modules/.bin/jiti +22 -0
- package/api-doc/node_modules/.bin/sass +22 -0
- package/api-doc/node_modules/.bin/tsx +22 -0
- package/api-doc/node_modules/.bin/vite +22 -0
- package/api-doc/node_modules/.bin/vitest +22 -0
- package/api-doc/node_modules/.bin/yaml +22 -0
- package/api-doc/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/api-doc/package.json +48 -0
- package/api-doc/src/api-doc.plugin.ts +554 -0
- package/api-doc/src/index.ts +5 -0
- package/api-doc/src/interfaces/renderer.interface.ts +5 -0
- package/api-doc/src/plugin.test.ts +740 -0
- package/api-doc/src/renderers/scalar.renderer.ts +82 -0
- package/api-doc/src/renderers/swagger.renderer.ts +89 -0
- package/api-doc/tsconfig.json +21 -0
- package/api-doc/vitest.config.ts +13 -0
- package/cache/.turbo/turbo-build.log +1 -0
- package/cache/.turbo/turbo-test.log +16 -0
- package/cache/CHANGELOG.md +23 -0
- package/cache/README.md +79 -0
- package/cache/dist/cache-service.d.ts +8 -0
- package/cache/dist/cache-service.js +21 -0
- package/cache/dist/cacheable.d.ts +2 -0
- package/cache/dist/cacheable.js +10 -0
- package/cache/dist/cloudflare/driver.d.ts +23 -0
- package/cache/dist/cloudflare/driver.js +102 -0
- package/cache/dist/cloudflare/index.d.ts +3 -0
- package/cache/dist/cloudflare/index.js +2 -0
- package/cache/dist/cloudflare/plugin.d.ts +19 -0
- package/cache/dist/cloudflare/plugin.js +58 -0
- package/cache/dist/index.d.ts +11 -0
- package/cache/dist/index.js +14 -0
- package/cache/dist/local/driver.d.ts +2 -0
- package/cache/dist/local/driver.js +98 -0
- package/cache/dist/local/index.d.ts +2 -0
- package/cache/dist/local/index.js +2 -0
- package/cache/dist/local/plugin.d.ts +14 -0
- package/cache/dist/local/plugin.js +24 -0
- package/cache/dist/redis/driver.d.ts +10 -0
- package/cache/dist/redis/driver.js +102 -0
- package/cache/dist/redis/index.d.ts +4 -0
- package/cache/dist/redis/index.js +2 -0
- package/cache/dist/redis/plugin.d.ts +25 -0
- package/cache/dist/redis/plugin.js +61 -0
- package/cache/dist/resolve.d.ts +24 -0
- package/cache/dist/resolve.js +114 -0
- package/cache/dist/types.d.ts +18 -0
- package/cache/dist/types.js +1 -0
- package/cache/node_modules/.bin/esbuild +22 -0
- package/cache/node_modules/.bin/jiti +22 -0
- package/cache/node_modules/.bin/sass +22 -0
- package/cache/node_modules/.bin/tsx +22 -0
- package/cache/node_modules/.bin/vite +22 -0
- package/cache/node_modules/.bin/vitest +22 -0
- package/cache/node_modules/.bin/yaml +22 -0
- package/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/cache/package.json +46 -0
- package/cache/src/cache-service.test.ts +16 -0
- package/cache/src/cache-service.ts +15 -0
- package/cache/src/cacheable.test.ts +78 -0
- package/cache/src/cacheable.ts +16 -0
- package/cache/src/cloudflare/driver.test.ts +188 -0
- package/cache/src/cloudflare/driver.ts +118 -0
- package/cache/src/cloudflare/index.ts +3 -0
- package/cache/src/cloudflare/plugin.ts +71 -0
- package/cache/src/index.test.ts +68 -0
- package/cache/src/index.ts +20 -0
- package/cache/src/local/driver.ts +117 -0
- package/cache/src/local/index.ts +2 -0
- package/cache/src/local/plugin.ts +34 -0
- package/cache/src/redis/driver.test.ts +71 -0
- package/cache/src/redis/driver.ts +124 -0
- package/cache/src/redis/index.ts +4 -0
- package/cache/src/redis/plugin.ts +84 -0
- package/cache/src/resolve.test.ts +257 -0
- package/cache/src/resolve.ts +140 -0
- package/cache/src/types.ts +14 -0
- package/cache/tsconfig.json +21 -0
- package/cache/vitest.config.ts +13 -0
- package/db/.turbo/turbo-build.log +1 -0
- package/db/.turbo/turbo-test.log +21 -0
- package/db/CHANGELOG.md +26 -0
- package/db/README.md +99 -0
- package/db/dist/constants.d.ts +1 -0
- package/db/dist/constants.js +1 -0
- package/db/dist/d1/index.d.ts +3 -0
- package/db/dist/d1/index.js +2 -0
- package/db/dist/d1/plugin.d.ts +18 -0
- package/db/dist/d1/plugin.js +104 -0
- package/db/dist/db-service.d.ts +7 -0
- package/db/dist/db-service.js +21 -0
- package/db/dist/index.d.ts +15 -0
- package/db/dist/index.js +20 -0
- package/db/dist/mysql/client.d.ts +36 -0
- package/db/dist/mysql/client.js +84 -0
- package/db/dist/mysql/driver.d.ts +18 -0
- package/db/dist/mysql/driver.js +107 -0
- package/db/dist/mysql/index.d.ts +11 -0
- package/db/dist/mysql/index.js +11 -0
- package/db/dist/mysql/plugin.d.ts +51 -0
- package/db/dist/mysql/plugin.js +94 -0
- package/db/dist/mysql/types.d.ts +38 -0
- package/db/dist/mysql/types.js +1 -0
- package/db/dist/postgres/index.d.ts +2 -0
- package/db/dist/postgres/index.js +3 -0
- package/db/dist/repository.d.ts +14 -0
- package/db/dist/repository.js +79 -0
- package/db/dist/resolve.d.ts +6 -0
- package/db/dist/resolve.js +81 -0
- package/db/dist/schema-builder/column-builder.d.ts +26 -0
- package/db/dist/schema-builder/column-builder.js +53 -0
- package/db/dist/schema-builder/compile-d1.d.ts +5 -0
- package/db/dist/schema-builder/compile-d1.js +67 -0
- package/db/dist/schema-builder/compile-mysql.d.ts +12 -0
- package/db/dist/schema-builder/compile-mysql.js +78 -0
- package/db/dist/schema-builder/index.d.ts +8 -0
- package/db/dist/schema-builder/index.js +22 -0
- package/db/dist/schema-builder/table-builder.d.ts +16 -0
- package/db/dist/schema-builder/table-builder.js +38 -0
- package/db/dist/schema-builder/types.d.ts +26 -0
- package/db/dist/schema-builder/types.js +1 -0
- package/db/dist/schema-builder/zod-bridge.d.ts +24 -0
- package/db/dist/schema-builder/zod-bridge.js +53 -0
- package/db/dist/sqlite/index.d.ts +4 -0
- package/db/dist/sqlite/index.js +2 -0
- package/db/dist/sqlite/plugin.d.ts +17 -0
- package/db/dist/sqlite/plugin.js +33 -0
- package/db/dist/types.d.ts +42 -0
- package/db/dist/types.js +1 -0
- package/db/node_modules/.bin/esbuild +22 -0
- package/db/node_modules/.bin/jiti +22 -0
- package/db/node_modules/.bin/sass +22 -0
- package/db/node_modules/.bin/tsx +22 -0
- package/db/node_modules/.bin/vite +22 -0
- package/db/node_modules/.bin/vitest +22 -0
- package/db/node_modules/.bin/yaml +22 -0
- package/db/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/db/package.json +59 -0
- package/db/src/constants.test.ts +8 -0
- package/db/src/constants.ts +1 -0
- package/db/src/d1/index.ts +13 -0
- package/db/src/d1/plugin.ts +135 -0
- package/db/src/db-service.ts +14 -0
- package/db/src/index.test.ts +63 -0
- package/db/src/index.ts +57 -0
- package/db/src/mysql/client.ts +112 -0
- package/db/src/mysql/driver.test.ts +190 -0
- package/db/src/mysql/driver.ts +116 -0
- package/db/src/mysql/index.ts +51 -0
- package/db/src/mysql/plugin.test.ts +228 -0
- package/db/src/mysql/plugin.ts +128 -0
- package/db/src/mysql/types.ts +32 -0
- package/db/src/postgres/index.ts +15 -0
- package/db/src/repository.test.ts +195 -0
- package/db/src/repository.ts +95 -0
- package/db/src/resolve.ts +105 -0
- package/db/src/schema-builder/column-builder.test.ts +55 -0
- package/db/src/schema-builder/column-builder.ts +87 -0
- package/db/src/schema-builder/compile-d1.test.ts +60 -0
- package/db/src/schema-builder/compile-d1.ts +70 -0
- package/db/src/schema-builder/compile-mysql.test.ts +64 -0
- package/db/src/schema-builder/compile-mysql.ts +102 -0
- package/db/src/schema-builder/index.test.ts +36 -0
- package/db/src/schema-builder/index.ts +41 -0
- package/db/src/schema-builder/table-builder.test.ts +55 -0
- package/db/src/schema-builder/table-builder.ts +50 -0
- package/db/src/schema-builder/types.test.ts +68 -0
- package/db/src/schema-builder/types.ts +30 -0
- package/db/src/schema-builder/zod-bridge.ts +89 -0
- package/db/src/sqlite/index.ts +14 -0
- package/db/src/sqlite/plugin.ts +46 -0
- package/db/src/types.ts +45 -0
- package/db/tsconfig.json +21 -0
- package/db/vitest.config.ts +13 -0
- package/mail/.turbo/turbo-build.log +1 -0
- package/mail/.turbo/turbo-test.log +12 -0
- package/mail/.turbo/turbo-typecheck.log +1 -0
- package/mail/CHANGELOG.md +25 -0
- package/mail/README.md +214 -0
- package/mail/dist/index.d.ts +4 -0
- package/mail/dist/index.js +2 -0
- package/mail/dist/plugin.d.ts +64 -0
- package/mail/dist/plugin.js +232 -0
- package/mail/dist/providers/brevo.provider.d.ts +15 -0
- package/mail/dist/providers/brevo.provider.js +75 -0
- package/mail/dist/providers/custom.provider.d.ts +9 -0
- package/mail/dist/providers/custom.provider.js +22 -0
- package/mail/dist/providers/resend.provider.d.ts +16 -0
- package/mail/dist/providers/resend.provider.js +77 -0
- package/mail/dist/providers/smtp.provider.d.ts +21 -0
- package/mail/dist/providers/smtp.provider.js +86 -0
- package/mail/dist/rate-limiter.d.ts +16 -0
- package/mail/dist/rate-limiter.js +49 -0
- package/mail/dist/types.d.ts +68 -0
- package/mail/dist/types.js +6 -0
- package/mail/node_modules/.bin/esbuild +22 -0
- package/mail/node_modules/.bin/jiti +22 -0
- package/mail/node_modules/.bin/sass +22 -0
- package/mail/node_modules/.bin/tsx +22 -0
- package/mail/node_modules/.bin/vite +22 -0
- package/mail/node_modules/.bin/vitest +22 -0
- package/mail/node_modules/.bin/yaml +22 -0
- package/mail/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/mail/package.json +52 -0
- package/mail/src/index.ts +4 -0
- package/mail/src/plugin.test.ts +219 -0
- package/mail/src/plugin.ts +313 -0
- package/mail/src/providers/brevo.provider.ts +78 -0
- package/mail/src/providers/custom.provider.ts +29 -0
- package/mail/src/providers/resend.provider.ts +74 -0
- package/mail/src/providers/smtp.provider.ts +85 -0
- package/mail/src/rate-limiter.test.ts +81 -0
- package/mail/src/rate-limiter.ts +54 -0
- package/mail/src/types.ts +81 -0
- package/mail/tsconfig.json +21 -0
- package/mail/vitest.config.ts +13 -0
- package/package.json +36 -0
- package/s3/.turbo/turbo-build.log +1 -0
- package/s3/.turbo/turbo-test.log +11 -0
- package/s3/CHANGELOG.md +20 -0
- package/s3/README.md +90 -0
- package/s3/dist/index.d.ts +6 -0
- package/s3/dist/index.js +4 -0
- package/s3/dist/s3-service.d.ts +7 -0
- package/s3/dist/s3-service.js +21 -0
- package/s3/dist/s3.driver.d.ts +6 -0
- package/s3/dist/s3.driver.js +221 -0
- package/s3/dist/s3.plugin.d.ts +24 -0
- package/s3/dist/s3.plugin.js +58 -0
- package/s3/dist/types.d.ts +30 -0
- package/s3/dist/types.js +1 -0
- package/s3/node_modules/.bin/esbuild +22 -0
- package/s3/node_modules/.bin/jiti +22 -0
- package/s3/node_modules/.bin/sass +22 -0
- package/s3/node_modules/.bin/tsx +22 -0
- package/s3/node_modules/.bin/vite +22 -0
- package/s3/node_modules/.bin/vitest +22 -0
- package/s3/node_modules/.bin/yaml +22 -0
- package/s3/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/s3/package.json +47 -0
- package/s3/src/index.test.ts +20 -0
- package/s3/src/index.ts +13 -0
- package/s3/src/s3-service.ts +14 -0
- package/s3/src/s3.driver.ts +259 -0
- package/s3/src/s3.plugin.ts +80 -0
- package/s3/src/types.ts +37 -0
- package/s3/tsconfig.json +21 -0
- package/s3/vitest.config.ts +13 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const { mockGetPlugin, mockGetPlugins, mockHasPlugin, mockRegisterPlugin } = vi.hoisted(() => ({
|
|
4
|
+
mockGetPlugin: vi.fn(),
|
|
5
|
+
mockGetPlugins: vi.fn(),
|
|
6
|
+
mockHasPlugin: vi.fn(),
|
|
7
|
+
mockRegisterPlugin: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock('@rx-ted/packages-honest', () => ({
|
|
11
|
+
ComponentManager: {
|
|
12
|
+
getPlugin: mockGetPlugin,
|
|
13
|
+
getPlugins: mockGetPlugins,
|
|
14
|
+
hasPlugin: mockHasPlugin,
|
|
15
|
+
registerPlugin: mockRegisterPlugin,
|
|
16
|
+
},
|
|
17
|
+
resolvePluginLogger: vi.fn().mockReturnValue({
|
|
18
|
+
child: vi.fn().mockReturnThis(),
|
|
19
|
+
info: vi.fn(),
|
|
20
|
+
error: vi.fn(),
|
|
21
|
+
warn: vi.fn(),
|
|
22
|
+
debug: vi.fn(),
|
|
23
|
+
}),
|
|
24
|
+
LOGGER_SYMBOL: Symbol('logger'),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock('./providers/resend.provider', () => ({
|
|
28
|
+
ResendMailProvider: class {
|
|
29
|
+
readonly name: string;
|
|
30
|
+
constructor(name: string) {
|
|
31
|
+
this.name = name;
|
|
32
|
+
}
|
|
33
|
+
send = vi.fn().mockResolvedValue({ id: 'resend-id', provider: 'resend' });
|
|
34
|
+
healthCheck = vi.fn().mockResolvedValue(true);
|
|
35
|
+
close = vi.fn().mockResolvedValue(undefined);
|
|
36
|
+
},
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
vi.mock('./providers/brevo.provider', () => ({
|
|
40
|
+
BrevoMailProvider: class {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
constructor(name: string) {
|
|
43
|
+
this.name = name;
|
|
44
|
+
}
|
|
45
|
+
send = vi.fn().mockResolvedValue({ id: 'brevo-id', provider: 'brevo' });
|
|
46
|
+
healthCheck = vi.fn().mockResolvedValue(true);
|
|
47
|
+
close = vi.fn().mockResolvedValue(undefined);
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
vi.mock('./providers/smtp.provider', () => ({
|
|
52
|
+
SmtpMailProvider: class {
|
|
53
|
+
readonly name: string;
|
|
54
|
+
constructor(name: string) {
|
|
55
|
+
this.name = name;
|
|
56
|
+
}
|
|
57
|
+
send = vi.fn().mockResolvedValue({ id: 'smtp-id', provider: 'smtp' });
|
|
58
|
+
healthCheck = vi.fn().mockResolvedValue(true);
|
|
59
|
+
close = vi.fn().mockResolvedValue(undefined);
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
import { MailPlugin, MAIL_DISABLED_MSG } from './plugin';
|
|
64
|
+
import { MailQuotaExceededError } from './types';
|
|
65
|
+
|
|
66
|
+
describe('MailPlugin', () => {
|
|
67
|
+
const resendOptions = { apiKey: 're_test_key', fromEmail: 'test@example.com' };
|
|
68
|
+
const brevoOptions = { apiKey: 'brevo_test_key', fromEmail: 'test@example.com' };
|
|
69
|
+
const smtpOptions = {
|
|
70
|
+
host: 'smtp.test.com',
|
|
71
|
+
port: 465,
|
|
72
|
+
user: 'user',
|
|
73
|
+
pass: 'pass',
|
|
74
|
+
fromEmail: 'test@example.com',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
vi.clearAllMocks();
|
|
79
|
+
mockGetPlugin.mockReturnValue(null);
|
|
80
|
+
mockHasPlugin.mockReturnValue(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('creates disabled MailPlugin with no options', () => {
|
|
84
|
+
const plugin = new MailPlugin();
|
|
85
|
+
expect(plugin).toBeInstanceOf(MailPlugin);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('getClient returns undefined when disabled', () => {
|
|
89
|
+
const plugin = new MailPlugin();
|
|
90
|
+
expect(plugin.getClient()).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('send throws when disabled', async () => {
|
|
94
|
+
const plugin = new MailPlugin();
|
|
95
|
+
await expect(
|
|
96
|
+
plugin.send({ from: 'a@b.com', to: 'user@test.com', subject: 'Test' }),
|
|
97
|
+
).rejects.toThrow(MAIL_DISABLED_MSG);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('healthCheck returns false when disabled', async () => {
|
|
101
|
+
const plugin = new MailPlugin();
|
|
102
|
+
const result = await plugin.healthCheck();
|
|
103
|
+
expect(result).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('beforeModulesRegistered registers all configured providers', async () => {
|
|
107
|
+
const plugin = new MailPlugin({
|
|
108
|
+
resend: resendOptions,
|
|
109
|
+
brevo: brevoOptions,
|
|
110
|
+
smtp: smtpOptions,
|
|
111
|
+
});
|
|
112
|
+
await plugin.beforeModulesRegistered({} as any, {} as any);
|
|
113
|
+
|
|
114
|
+
expect(mockRegisterPlugin).toHaveBeenCalledWith('mail:resend', expect.any(Object));
|
|
115
|
+
expect(mockRegisterPlugin).toHaveBeenCalledWith('mail:brevo', expect.any(Object));
|
|
116
|
+
expect(mockRegisterPlugin).toHaveBeenCalledWith('mail:smtp', expect.any(Object));
|
|
117
|
+
expect(mockRegisterPlugin).toHaveBeenCalledWith('app:mail', plugin);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('getClient returns provider by name', async () => {
|
|
121
|
+
const plugin = new MailPlugin({ resend: resendOptions, brevo: brevoOptions });
|
|
122
|
+
await plugin.beforeModulesRegistered({} as any, {} as any);
|
|
123
|
+
|
|
124
|
+
expect(plugin.getClient('resend')?.name).toBe('resend');
|
|
125
|
+
expect(plugin.getClient('brevo')?.name).toBe('brevo');
|
|
126
|
+
expect(plugin.getClient()).toBeDefined();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('getClient returns undefined for unknown name', async () => {
|
|
130
|
+
const plugin = new MailPlugin({ resend: resendOptions });
|
|
131
|
+
await plugin.beforeModulesRegistered({} as any, {} as any);
|
|
132
|
+
expect(plugin.getClient('nonexistent')).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('send without providerName tries resend first', async () => {
|
|
136
|
+
const plugin = new MailPlugin({
|
|
137
|
+
resend: resendOptions,
|
|
138
|
+
brevo: brevoOptions,
|
|
139
|
+
smtp: smtpOptions,
|
|
140
|
+
});
|
|
141
|
+
await plugin.beforeModulesRegistered({} as any, {} as any);
|
|
142
|
+
|
|
143
|
+
const result = await plugin.send({ to: 'user@test.com', subject: 'Test' });
|
|
144
|
+
expect(result.id).toBe('resend-id');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('send with providerName uses that specific provider', async () => {
|
|
148
|
+
const plugin = new MailPlugin({
|
|
149
|
+
resend: resendOptions,
|
|
150
|
+
brevo: brevoOptions,
|
|
151
|
+
smtp: smtpOptions,
|
|
152
|
+
});
|
|
153
|
+
await plugin.beforeModulesRegistered({} as any, {} as any);
|
|
154
|
+
|
|
155
|
+
const result = await plugin.send({ to: 'user@test.com', subject: 'Test' }, 'smtp');
|
|
156
|
+
expect(result.id).toBe('smtp-id');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('send with unknown providerName throws', async () => {
|
|
160
|
+
const plugin = new MailPlugin({ resend: resendOptions });
|
|
161
|
+
await plugin.beforeModulesRegistered({} as any, {} as any);
|
|
162
|
+
|
|
163
|
+
await expect(plugin.send({ to: 'user@test.com', subject: 'Test' }, 'nope')).rejects.toThrow(
|
|
164
|
+
/unknown provider/i,
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('send falls through to next provider when first fails', async () => {
|
|
169
|
+
const plugin = new MailPlugin({ resend: resendOptions, smtp: smtpOptions });
|
|
170
|
+
await plugin.beforeModulesRegistered({} as any, {} as any);
|
|
171
|
+
|
|
172
|
+
const resendProvider = plugin.getClient('resend')!;
|
|
173
|
+
vi.spyOn(resendProvider, 'send').mockRejectedValue(new Error('Resend down'));
|
|
174
|
+
|
|
175
|
+
const result = await plugin.send({ to: 'user@test.com', subject: 'Test' });
|
|
176
|
+
expect(result.id).toBe('smtp-id');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('send throws when all providers fail', async () => {
|
|
180
|
+
const plugin = new MailPlugin({ resend: resendOptions });
|
|
181
|
+
await plugin.beforeModulesRegistered({} as any, {} as any);
|
|
182
|
+
|
|
183
|
+
vi.spyOn(plugin.getClient('resend')!, 'send').mockRejectedValue(new Error('Down'));
|
|
184
|
+
|
|
185
|
+
await expect(plugin.send({ to: 'user@test.com', subject: 'Test' })).rejects.toThrow(
|
|
186
|
+
/All providers failed/,
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('beforeModulesRegistered stays disabled when no config', async () => {
|
|
191
|
+
const plugin = new MailPlugin({});
|
|
192
|
+
await plugin.beforeModulesRegistered({} as any, {} as any);
|
|
193
|
+
|
|
194
|
+
expect(plugin.getClient()).toBeUndefined();
|
|
195
|
+
expect(mockRegisterPlugin).toHaveBeenCalledWith('app:mail', plugin);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('healthCheck delegates to first provider', async () => {
|
|
199
|
+
const plugin = new MailPlugin({ resend: resendOptions, smtp: smtpOptions });
|
|
200
|
+
await plugin.beforeModulesRegistered({} as any, {} as any);
|
|
201
|
+
|
|
202
|
+
const result = await plugin.healthCheck();
|
|
203
|
+
expect(result).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('close calls close on all providers', async () => {
|
|
207
|
+
const plugin = new MailPlugin({ resend: resendOptions, brevo: brevoOptions });
|
|
208
|
+
await plugin.beforeModulesRegistered({} as any, {} as any);
|
|
209
|
+
|
|
210
|
+
await plugin.close();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('MailQuotaExceededError can be thrown and caught', () => {
|
|
214
|
+
const err = new MailQuotaExceededError('daily', 100);
|
|
215
|
+
expect(err).toBeInstanceOf(Error);
|
|
216
|
+
expect(err.message).toMatch(/daily/i);
|
|
217
|
+
expect(err.name).toBe('MailQuotaExceededError');
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { ENV_SYMBOL, type Env, type ILogger } from '@rx-ted/packages-core';
|
|
2
|
+
import { ComponentManager, resolvePluginLogger } from '@rx-ted/packages-honest';
|
|
3
|
+
import type { Application, IPlugin } from '@rx-ted/packages-honest';
|
|
4
|
+
import type { SendMailOptions, SendMailResult } from './types';
|
|
5
|
+
import type { MailProvider } from './types';
|
|
6
|
+
import { RateLimiter, type MailCacheDriver } from './rate-limiter';
|
|
7
|
+
import { ResendMailProvider } from './providers/resend.provider';
|
|
8
|
+
import { BrevoMailProvider } from './providers/brevo.provider';
|
|
9
|
+
import { SmtpMailProvider } from './providers/smtp.provider';
|
|
10
|
+
|
|
11
|
+
export const MAIL_GLOBAL_KEY = 'app:mail';
|
|
12
|
+
|
|
13
|
+
export interface MailPluginOptions {
|
|
14
|
+
resend?: {
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
fromEmail?: string;
|
|
17
|
+
fromName?: string;
|
|
18
|
+
dailyQuota?: number;
|
|
19
|
+
};
|
|
20
|
+
brevo?: {
|
|
21
|
+
apiKey?: string;
|
|
22
|
+
fromEmail?: string;
|
|
23
|
+
fromName?: string;
|
|
24
|
+
dailyQuota?: number;
|
|
25
|
+
};
|
|
26
|
+
smtp?: {
|
|
27
|
+
host?: string;
|
|
28
|
+
port?: number;
|
|
29
|
+
user?: string;
|
|
30
|
+
pass?: string;
|
|
31
|
+
fromEmail?: string;
|
|
32
|
+
fromName?: string;
|
|
33
|
+
secure?: boolean;
|
|
34
|
+
dailyQuota?: number;
|
|
35
|
+
};
|
|
36
|
+
fromEmail?: string;
|
|
37
|
+
fromName?: string;
|
|
38
|
+
healthCheck?: {
|
|
39
|
+
enabled?: boolean;
|
|
40
|
+
intervalMs?: number;
|
|
41
|
+
maxRetries?: number;
|
|
42
|
+
retryDelayMs?: number;
|
|
43
|
+
};
|
|
44
|
+
warmUpTimeout?: number;
|
|
45
|
+
sendTimeout?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const MAIL_DISABLED_MSG =
|
|
49
|
+
'Mail service is not configured. Configure resend, brevo, or smtp provider.';
|
|
50
|
+
|
|
51
|
+
export class MailPlugin implements IPlugin, MailProvider {
|
|
52
|
+
readonly name = MAIL_GLOBAL_KEY;
|
|
53
|
+
readonly version = '0.0.1';
|
|
54
|
+
logger?: ILogger;
|
|
55
|
+
|
|
56
|
+
private providers: MailProvider[] = [];
|
|
57
|
+
private rateLimiters: Map<string, RateLimiter> = new Map();
|
|
58
|
+
private readonly options: MailPluginOptions;
|
|
59
|
+
private failedProviders = new Set<string>();
|
|
60
|
+
private isWarming = false;
|
|
61
|
+
|
|
62
|
+
constructor(options: MailPluginOptions = {}) {
|
|
63
|
+
this.options = options;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getClient(name?: string): MailProvider | undefined {
|
|
67
|
+
if (name) return this.providers.find((p) => p.name === name);
|
|
68
|
+
return this.providers[0];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async warmUp(): Promise<void> {
|
|
72
|
+
if (this.isWarming) return;
|
|
73
|
+
this.isWarming = true;
|
|
74
|
+
|
|
75
|
+
const timeout = this.options.warmUpTimeout ?? 10_000;
|
|
76
|
+
const relevant = this.providers.filter((p) => typeof p.warmUp === 'function');
|
|
77
|
+
if (relevant.length === 0) {
|
|
78
|
+
this.isWarming = false;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const results = await Promise.allSettled(
|
|
83
|
+
relevant.map((p) =>
|
|
84
|
+
(async () => {
|
|
85
|
+
try {
|
|
86
|
+
await Promise.race([
|
|
87
|
+
p.warmUp!(),
|
|
88
|
+
new Promise<never>((_, reject) =>
|
|
89
|
+
setTimeout(() => reject(new Error('Warm-up timeout')), timeout),
|
|
90
|
+
),
|
|
91
|
+
]);
|
|
92
|
+
this.logger?.info('[mail] Provider warmed up', { provider: p.name });
|
|
93
|
+
} catch (err) {
|
|
94
|
+
this.failedProviders.add(p.name);
|
|
95
|
+
this.logger?.warn('[mail] Provider warm-up failed or timed out', {
|
|
96
|
+
provider: p.name,
|
|
97
|
+
error: err instanceof Error ? err.message : String(err),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
})(),
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const succeeded = results.filter((r) => r.status === 'fulfilled').length;
|
|
105
|
+
this.logger?.info('[mail] Warm-up completed', { total: relevant.length, succeeded });
|
|
106
|
+
this.isWarming = false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async send(options: SendMailOptions, providerName?: string): Promise<SendMailResult> {
|
|
110
|
+
if (this.providers.length === 0) {
|
|
111
|
+
throw new Error(MAIL_DISABLED_MSG);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (providerName) {
|
|
115
|
+
const provider = this.providers.find((p) => p.name === providerName);
|
|
116
|
+
if (!provider) throw new Error(`Unknown provider "${providerName}"`);
|
|
117
|
+
if (this.failedProviders.has(providerName)) {
|
|
118
|
+
throw new Error(`Provider "${providerName}" unavailable (warm-up failed)`);
|
|
119
|
+
}
|
|
120
|
+
const limiter = this.rateLimiters.get(providerName);
|
|
121
|
+
if (limiter) await limiter.checkAndIncrement();
|
|
122
|
+
return provider.send(options);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const errors: Error[] = [];
|
|
126
|
+
for (const provider of this.providers) {
|
|
127
|
+
if (this.failedProviders.has(provider.name)) continue;
|
|
128
|
+
try {
|
|
129
|
+
const limiter = this.rateLimiters.get(provider.name);
|
|
130
|
+
if (limiter) await limiter.checkAndIncrement();
|
|
131
|
+
return await provider.send(options);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
errors.push(err as Error);
|
|
134
|
+
this.logger?.warn?.('Provider failed, trying next', {
|
|
135
|
+
provider: provider.name,
|
|
136
|
+
error: err,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`All providers failed: ${errors.map((e) => e.message).join('; ')}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
healthCheck(): Promise<boolean> {
|
|
144
|
+
if (this.providers.length === 0) return Promise.resolve(false);
|
|
145
|
+
return this.providers[0].healthCheck();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private tryInitResend(
|
|
149
|
+
cfg: NonNullable<MailPluginOptions['resend']>,
|
|
150
|
+
cacheDrv: MailCacheDriver | null,
|
|
151
|
+
appEnv: Env | undefined,
|
|
152
|
+
): void {
|
|
153
|
+
const apiKey = cfg.apiKey ?? appEnv?.get('MAIL_RESEND_API_KEY');
|
|
154
|
+
if (!apiKey) return;
|
|
155
|
+
const fromEmail =
|
|
156
|
+
cfg.fromEmail ??
|
|
157
|
+
appEnv?.var('MAIL_RESEND_FROM_EMAIL', 'noreply@19981204.xyz') ??
|
|
158
|
+
'noreply@19981204.xyz';
|
|
159
|
+
const fromName = cfg.fromName ?? appEnv?.var('MAIL_RESEND_FROM_NAME', 'noreply') ?? 'noreply';
|
|
160
|
+
this.providers.push(new ResendMailProvider('resend', { apiKey }, fromEmail, fromName));
|
|
161
|
+
this.rateLimiters.set(
|
|
162
|
+
'resend',
|
|
163
|
+
new RateLimiter(cfg.dailyQuota ?? Infinity, 'resend', cacheDrv, this.logger),
|
|
164
|
+
);
|
|
165
|
+
this.logger?.info('Mail provider registered', { name: 'resend' });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private tryInitBrevo(
|
|
169
|
+
cfg: NonNullable<MailPluginOptions['brevo']>,
|
|
170
|
+
cacheDrv: MailCacheDriver | null,
|
|
171
|
+
appEnv: Env | undefined,
|
|
172
|
+
): void {
|
|
173
|
+
const apiKey = cfg.apiKey ?? appEnv?.get('MAIL_BREVO_API_KEY');
|
|
174
|
+
if (!apiKey) return;
|
|
175
|
+
const fromEmail =
|
|
176
|
+
cfg.fromEmail ??
|
|
177
|
+
appEnv?.var('MAIL_BREVO_FROM_EMAIL', 'noreply@19981204.xyz') ??
|
|
178
|
+
'noreply@19981204.xyz';
|
|
179
|
+
const fromName = cfg.fromName ?? appEnv?.var('MAIL_BREVO_FROM_NAME', 'noreply') ?? 'noreply';
|
|
180
|
+
this.providers.push(new BrevoMailProvider('brevo', { apiKey }, fromEmail, fromName));
|
|
181
|
+
this.rateLimiters.set(
|
|
182
|
+
'brevo',
|
|
183
|
+
new RateLimiter(cfg.dailyQuota ?? Infinity, 'brevo', cacheDrv, this.logger),
|
|
184
|
+
);
|
|
185
|
+
this.logger?.info('Mail provider registered', { name: 'brevo' });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private tryInitSmtp(
|
|
189
|
+
cfg: NonNullable<MailPluginOptions['smtp']>,
|
|
190
|
+
cacheDrv: MailCacheDriver | null,
|
|
191
|
+
appEnv: Env | undefined,
|
|
192
|
+
): void {
|
|
193
|
+
const host = cfg.host ?? appEnv?.get('MAIL_SMTP_HOST');
|
|
194
|
+
const user = cfg.user ?? appEnv?.get('MAIL_SMTP_USER');
|
|
195
|
+
const pass = cfg.pass ?? appEnv?.get('MAIL_SMTP_PASS');
|
|
196
|
+
const port = cfg.port ?? appEnv?.get('MAIL_SMTP_PORT', 'number') ?? 587;
|
|
197
|
+
if (!host || !user || !pass) return;
|
|
198
|
+
const fromEmail = (cfg.fromEmail ?? appEnv?.var('MAIL_SMTP_FROM_EMAIL')) || user;
|
|
199
|
+
const fromName = cfg.fromName ?? appEnv?.var('MAIL_SMTP_FROM_NAME', 'rx-ted') ?? 'rx-ted';
|
|
200
|
+
this.providers.push(
|
|
201
|
+
new SmtpMailProvider(
|
|
202
|
+
'smtp',
|
|
203
|
+
{ host, port, user, pass, secure: cfg.secure },
|
|
204
|
+
fromEmail,
|
|
205
|
+
fromName,
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
this.rateLimiters.set(
|
|
209
|
+
'smtp',
|
|
210
|
+
new RateLimiter(cfg.dailyQuota ?? Infinity, 'smtp', cacheDrv, this.logger),
|
|
211
|
+
);
|
|
212
|
+
this.logger?.info('Mail provider registered', { name: 'smtp' });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async beforeModulesRegistered(app: Application, _hono: unknown): Promise<void> {
|
|
216
|
+
this.logger ??= resolvePluginLogger(this.name);
|
|
217
|
+
const cacheDrv = this.getCacheDriver(app);
|
|
218
|
+
const appEnv = this.getEnv();
|
|
219
|
+
|
|
220
|
+
this.tryInitResend(this.options.resend ?? {}, cacheDrv, appEnv);
|
|
221
|
+
this.tryInitBrevo(this.options.brevo ?? {}, cacheDrv, appEnv);
|
|
222
|
+
this.tryInitSmtp(this.options.smtp ?? {}, cacheDrv, appEnv);
|
|
223
|
+
|
|
224
|
+
if (this.providers.length === 0) {
|
|
225
|
+
this.logger.warn(MAIL_DISABLED_MSG);
|
|
226
|
+
} else {
|
|
227
|
+
for (const p of this.providers) {
|
|
228
|
+
ComponentManager.registerPlugin(`mail:${p.name}`, p);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
ComponentManager.registerPlugin(MAIL_GLOBAL_KEY, this);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async afterModulesRegistered(_app: Application, _hono: unknown): Promise<void> {
|
|
236
|
+
if (this.providers.length === 0 || !this.logger) return;
|
|
237
|
+
if (this.options.healthCheck?.enabled === false) {
|
|
238
|
+
this.logger.info('Health check disabled by config');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
this.logger.info('Health check deferred to background (will run via waitUntil)');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async runHealthChecks(): Promise<void> {
|
|
245
|
+
if (this.providers.length === 0 || !this.logger) return;
|
|
246
|
+
if (this.options.healthCheck?.enabled === false) return;
|
|
247
|
+
|
|
248
|
+
const maxRetries = this.options.healthCheck?.maxRetries ?? 3;
|
|
249
|
+
const retryDelay = this.options.healthCheck?.retryDelayMs ?? 1000;
|
|
250
|
+
|
|
251
|
+
for (const provider of this.providers) {
|
|
252
|
+
const limiter = this.rateLimiters.get(provider.name);
|
|
253
|
+
if (!limiter) continue;
|
|
254
|
+
const done = await limiter.wasHealthCheckDoneToday();
|
|
255
|
+
if (done) {
|
|
256
|
+
this.logger.info('Health check already done today', { provider: provider.name });
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
let success = false;
|
|
260
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
261
|
+
this.logger.info('Running health check', { provider: provider.name, attempt });
|
|
262
|
+
success = await provider.healthCheck();
|
|
263
|
+
if (success) break;
|
|
264
|
+
if (attempt < maxRetries) await sleep(retryDelay);
|
|
265
|
+
}
|
|
266
|
+
if (success) {
|
|
267
|
+
await limiter.markHealthCheckDone();
|
|
268
|
+
this.logger.info('Health check passed', { provider: provider.name });
|
|
269
|
+
} else {
|
|
270
|
+
this.logger.warn('Health check failed after retries', {
|
|
271
|
+
provider: provider.name,
|
|
272
|
+
maxRetries,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async close(): Promise<void> {
|
|
279
|
+
for (const provider of this.providers) {
|
|
280
|
+
try {
|
|
281
|
+
await provider.close();
|
|
282
|
+
} catch (e) {
|
|
283
|
+
this.logger?.error?.('Error closing provider', { provider: provider.name, error: e });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private getCacheDriver(_app: Application): MailCacheDriver | null {
|
|
289
|
+
try {
|
|
290
|
+
if (ComponentManager.hasPlugin('cache')) {
|
|
291
|
+
return ComponentManager.getPlugin<MailCacheDriver>('cache');
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private getEnv(): Env | undefined {
|
|
300
|
+
try {
|
|
301
|
+
if (ComponentManager.hasPlugin(ENV_SYMBOL)) {
|
|
302
|
+
return ComponentManager.getPlugin<Env>(ENV_SYMBOL);
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function sleep(ms: number): Promise<void> {
|
|
312
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
313
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { BrevoClient } from '@getbrevo/brevo';
|
|
2
|
+
import type { MailProvider, SendMailOptions, SendMailResult } from '../types';
|
|
3
|
+
|
|
4
|
+
export class BrevoMailProvider implements MailProvider {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
private client: BrevoClient | null = null;
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
name: string,
|
|
10
|
+
private config: { apiKey: string },
|
|
11
|
+
private fromEmail: string,
|
|
12
|
+
private fromName?: string,
|
|
13
|
+
) {
|
|
14
|
+
this.name = name;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private async ensureInit(): Promise<void> {
|
|
18
|
+
if (this.client) return;
|
|
19
|
+
this.client = new BrevoClient({ apiKey: this.config.apiKey });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async send(options: SendMailOptions, _providerName?: string): Promise<SendMailResult> {
|
|
23
|
+
await this.ensureInit();
|
|
24
|
+
const to = (Array.isArray(options.to) ? options.to : [options.to]).map((e: string) => ({
|
|
25
|
+
email: e,
|
|
26
|
+
}));
|
|
27
|
+
const payload: Record<string, unknown> = {
|
|
28
|
+
sender: { name: this.fromName ?? '', email: options.from || this.fromEmail },
|
|
29
|
+
to,
|
|
30
|
+
subject: options.subject,
|
|
31
|
+
};
|
|
32
|
+
if (options.html) payload.htmlContent = options.html;
|
|
33
|
+
if (options.text) payload.textContent = options.text;
|
|
34
|
+
if (options.cc)
|
|
35
|
+
payload.cc = (Array.isArray(options.cc) ? options.cc : [options.cc]).map((e: string) => ({
|
|
36
|
+
email: e,
|
|
37
|
+
}));
|
|
38
|
+
if (options.bcc)
|
|
39
|
+
payload.bcc = (Array.isArray(options.bcc) ? options.bcc : [options.bcc]).map((e: string) => ({
|
|
40
|
+
email: e,
|
|
41
|
+
}));
|
|
42
|
+
if (options.attachments?.length) {
|
|
43
|
+
payload.attachment = options.attachments.map((a) => ({
|
|
44
|
+
name: a.filename,
|
|
45
|
+
content: a.content instanceof Uint8Array ? uint8ArrayToBase64(a.content) : a.content,
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
const result = await (this.client!.transactionalEmails.sendTransacEmail as any)(payload);
|
|
49
|
+
return { id: result?.messageId ?? 'unknown', provider: this.name };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async healthCheck(): Promise<boolean> {
|
|
53
|
+
try {
|
|
54
|
+
await this.ensureInit();
|
|
55
|
+
await (this.client!.transactionalEmails.sendTransacEmail as any)({
|
|
56
|
+
sender: { name: this.fromName ?? '', email: this.fromEmail },
|
|
57
|
+
to: [{ email: this.fromEmail }],
|
|
58
|
+
subject: 'Health check',
|
|
59
|
+
textContent: 'This is a health check email from Brevo provider.',
|
|
60
|
+
});
|
|
61
|
+
return true;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async close(): Promise<void> {
|
|
68
|
+
this.client = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function uint8ArrayToBase64(u8: Uint8Array): string {
|
|
73
|
+
let binary = '';
|
|
74
|
+
for (let i = 0; i < u8.length; i++) {
|
|
75
|
+
binary += String.fromCharCode(u8[i]);
|
|
76
|
+
}
|
|
77
|
+
return btoa(binary);
|
|
78
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { MailProvider, SendMailOptions, SendMailResult } from '../types';
|
|
2
|
+
|
|
3
|
+
export class CustomMailProvider implements MailProvider {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
|
|
6
|
+
constructor(
|
|
7
|
+
name: string,
|
|
8
|
+
private impl: MailProvider,
|
|
9
|
+
) {
|
|
10
|
+
this.name = name;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async send(options: SendMailOptions, providerName?: string): Promise<SendMailResult> {
|
|
14
|
+
return this.impl.send(options, providerName);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async healthCheck(): Promise<boolean> {
|
|
18
|
+
if (typeof this.impl.healthCheck === 'function') {
|
|
19
|
+
return this.impl.healthCheck();
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async close(): Promise<void> {
|
|
25
|
+
if (typeof this.impl.close === 'function') {
|
|
26
|
+
await this.impl.close();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|