@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,12 @@
|
|
|
1
|
+
$ vitest run
|
|
2
|
+
|
|
3
|
+
[1m[30m[46m RUN [49m[39m[22m [36mv4.1.8 [39m[90m/Users/ben/projects/app/packages/honest-plugins/mail[39m
|
|
4
|
+
|
|
5
|
+
[32m✓[39m src/rate-limiter.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
6
|
+
[32m✓[39m src/plugin.test.ts [2m([22m[2m16 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
7
|
+
|
|
8
|
+
[2m Test Files [22m [1m[32m2 passed[39m[22m[90m (2)[39m
|
|
9
|
+
[2m Tests [22m [1m[32m24 passed[39m[22m[90m (24)[39m
|
|
10
|
+
[2m Start at [22m 14:17:13
|
|
11
|
+
[2m Duration [22m 137ms[2m (transform 69ms, setup 0ms, import 91ms, tests 12ms, environment 0ms)[22m
|
|
12
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
$ tsc -p tsconfig.json --noEmit
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @rx-ted/packages-honest-plugins-mail
|
|
2
|
+
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 11bbae7: Remove defunct packages (web-admin, auth, http-client, search) and introduce barrel packages for consolidated exports. Migrate all internal imports from scoped plugin names (`@rx-ted/packages-honest-plugins-db`) to path-based barrel imports (`@rx-ted/packages-honest-plugins/db`). Reset all package versions to 1.0.0 with consolidated changelogs. Clean up obsolete documentation across the monorepo.
|
|
8
|
+
- Updated dependencies [11bbae7]
|
|
9
|
+
- @rx-ted/packages-core@1.0.1
|
|
10
|
+
- @rx-ted/packages-honest@1.0.1
|
|
11
|
+
|
|
12
|
+
## 1.0.0
|
|
13
|
+
|
|
14
|
+
Initial consolidated release.
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
- Mail plugin for @rx-ted/packages-honest
|
|
19
|
+
- Multiple mail providers (Resend, Brevo, SMTP, Custom)
|
|
20
|
+
- Rate limiting, health check, and quota management
|
|
21
|
+
- `warmUp(provider)` — eagerly establishes SMTP connection before first send
|
|
22
|
+
- `failedProviders` Set — tracks providers that failed warmUp/health check
|
|
23
|
+
- `runHealthChecks()` — periodic background health check for all providers
|
|
24
|
+
- `warmUpTimeout` / `sendTimeout` config options
|
|
25
|
+
- Pull-based configuration from ComponentManager caches
|
package/mail/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# @rx-ted/packages-honest-plugins-mail
|
|
2
|
+
|
|
3
|
+
Mail plugin for [@rx-ted/packages-honest](https://github.com/rx-ted/honest). Supports multiple email providers with automatic failover, per-provider daily rate limiting, and health checks.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multi-provider**: Resend, Brevo, SMTP, or custom — up to 2 providers
|
|
8
|
+
- **Auto-failover**: tries the next provider if the primary fails
|
|
9
|
+
- **Rate limiting**: per-provider daily quota via cache (`incr`/`expire`)
|
|
10
|
+
- **Health checks**: verifies each provider once per day with configurable retries
|
|
11
|
+
- **Runtime-agnostic**: works on Node.js, Bun, Deno, and Cloudflare Workers
|
|
12
|
+
- **Lazy imports**: optional SDKs (`resend`, `@getbrevo/brevo`, `sently`) are loaded only when needed
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add @rx-ted/packages-honest-plugins-mail
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Peer dependencies: `@rx-ted/packages-honest`, `hono`
|
|
21
|
+
|
|
22
|
+
Optional SDKs (install only the providers you need):
|
|
23
|
+
```bash
|
|
24
|
+
pnpm add resend # for Resend provider
|
|
25
|
+
pnpm add sently # for SMTP provider
|
|
26
|
+
pnpm add @getbrevo/brevo # for Brevo provider
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### Register as a plugin
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { MailPlugin } from '@rx-ted/packages-honest-plugins-mail';
|
|
35
|
+
import type { MailPluginOptions } from '@rx-ted/packages-honest-plugins-mail';
|
|
36
|
+
|
|
37
|
+
const plugin = new MailPlugin({
|
|
38
|
+
providers: [
|
|
39
|
+
{
|
|
40
|
+
name: 'primary',
|
|
41
|
+
provider: 'resend',
|
|
42
|
+
fromEmail: 'noreply@example.com',
|
|
43
|
+
resend: { apiKey: 're_xxx' },
|
|
44
|
+
limits: { dailyQuota: 100 },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'fallback',
|
|
48
|
+
provider: 'smtp',
|
|
49
|
+
fromEmail: 'noreply@example.com',
|
|
50
|
+
smtp: {
|
|
51
|
+
host: 'smtp.example.com',
|
|
52
|
+
port: 465,
|
|
53
|
+
user: 'user',
|
|
54
|
+
pass: 'pass',
|
|
55
|
+
secure: true,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
healthCheck: {
|
|
60
|
+
intervalMs: 300_000, // check every 5 minutes
|
|
61
|
+
maxRetries: 3,
|
|
62
|
+
retryDelayMs: 1000,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Then pass the plugin to `Application.create`:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
const { hono } = await Application.create(AppModule, {
|
|
71
|
+
plugins: [plugin],
|
|
72
|
+
// ...
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Send an email
|
|
77
|
+
|
|
78
|
+
The primary provider is registered under `'mail'` in `ComponentManager`. Individual providers are registered under `'mail:<name>'`.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { ComponentManager } from '@rx-ted/packages-honest';
|
|
82
|
+
import type { MailProvider } from '@rx-ted/packages-honest-plugins-mail';
|
|
83
|
+
|
|
84
|
+
const provider = ComponentManager.getPlugin<MailProvider>('mail');
|
|
85
|
+
const result = await provider.send({
|
|
86
|
+
from: 'noreply@example.com',
|
|
87
|
+
to: 'user@example.com',
|
|
88
|
+
subject: 'Hello',
|
|
89
|
+
html: '<h1>Hello</h1>',
|
|
90
|
+
text: 'Hello',
|
|
91
|
+
});
|
|
92
|
+
// { id: '...', provider: 'primary' }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Get a specific provider
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const provider = mailPlugin.getClient('brevo-provider');
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Provider Configuration
|
|
102
|
+
|
|
103
|
+
### Resend
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
{
|
|
107
|
+
name: 'resend',
|
|
108
|
+
provider: 'resend',
|
|
109
|
+
fromEmail: 'noreply@example.com',
|
|
110
|
+
fromName: 'My App', // optional
|
|
111
|
+
resend: { apiKey: 're_xxx' },
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Brevo
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
{
|
|
119
|
+
name: 'brevo',
|
|
120
|
+
provider: 'brevo',
|
|
121
|
+
fromEmail: 'noreply@example.com',
|
|
122
|
+
brevo: { apiKey: 'xxx' },
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### SMTP
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
{
|
|
130
|
+
name: 'smtp',
|
|
131
|
+
provider: 'smtp',
|
|
132
|
+
fromEmail: 'noreply@example.com',
|
|
133
|
+
smtp: {
|
|
134
|
+
host: 'smtp.example.com',
|
|
135
|
+
port: 465,
|
|
136
|
+
user: 'username',
|
|
137
|
+
pass: 'password',
|
|
138
|
+
secure: true, // false for port 587 (STARTTLS)
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
> **Note on Cloudflare Workers:** SMTP uses `sently` which relies on `cloudflare:sockets` TCP API. Port 25 is blocked; use 465 or 587.
|
|
144
|
+
|
|
145
|
+
### Custom
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { CustomMailProvider } from '@rx-ted/packages-honest-plugins-mail';
|
|
149
|
+
|
|
150
|
+
{
|
|
151
|
+
name: 'custom',
|
|
152
|
+
provider: 'custom',
|
|
153
|
+
custom: myCustomMailProvider, // any MailProvider instance
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Rate Limiting
|
|
158
|
+
|
|
159
|
+
When a cache driver is available (e.g., `RedisPlugin`), the `RateLimiter` enforces daily quotas via cache `incr`/`expire`. If no cache is registered, rate limiting is disabled.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
{
|
|
163
|
+
name: 'resend',
|
|
164
|
+
provider: 'resend',
|
|
165
|
+
fromEmail: '...',
|
|
166
|
+
resend: { apiKey: '...' },
|
|
167
|
+
limits: { dailyQuota: 1000 },
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Health Checks
|
|
172
|
+
|
|
173
|
+
The plugin runs health checks in `afterModulesRegistered` with configurable retries. Each provider is checked once per day (tracked via cache).
|
|
174
|
+
|
|
175
|
+
## API
|
|
176
|
+
|
|
177
|
+
### `MailPlugin`
|
|
178
|
+
|
|
179
|
+
| Method | Description |
|
|
180
|
+
|--------|-------------|
|
|
181
|
+
| `getClient(name?)` | Returns the primary provider or a named provider |
|
|
182
|
+
| `beforeModulesRegistered(app, hono)` | Registers providers in ComponentManager |
|
|
183
|
+
| `afterModulesRegistered(app, hono)` | Runs initial health checks |
|
|
184
|
+
| `close()` | Closes all provider connections |
|
|
185
|
+
|
|
186
|
+
### Types
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
interface MailProvider {
|
|
190
|
+
readonly name: string;
|
|
191
|
+
send(options: SendMailOptions): Promise<SendMailResult>;
|
|
192
|
+
healthCheck(): Promise<boolean>;
|
|
193
|
+
close(): Promise<void>;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
interface SendMailOptions {
|
|
197
|
+
from: string;
|
|
198
|
+
to: string | string[];
|
|
199
|
+
cc?: string | string[];
|
|
200
|
+
bcc?: string | string[];
|
|
201
|
+
replyTo?: string;
|
|
202
|
+
subject: string;
|
|
203
|
+
text?: string;
|
|
204
|
+
html?: string;
|
|
205
|
+
attachments?: Attachment[];
|
|
206
|
+
headers?: Record<string, string>;
|
|
207
|
+
tags?: Record<string, string>;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
interface SendMailResult {
|
|
211
|
+
id: string;
|
|
212
|
+
provider: string;
|
|
213
|
+
}
|
|
214
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type ILogger } from '@rx-ted/packages-core';
|
|
2
|
+
import type { Application, IPlugin } from '@rx-ted/packages-honest';
|
|
3
|
+
import type { SendMailOptions, SendMailResult } from './types';
|
|
4
|
+
import type { MailProvider } from './types';
|
|
5
|
+
export declare const MAIL_GLOBAL_KEY = "app:mail";
|
|
6
|
+
export interface MailPluginOptions {
|
|
7
|
+
resend?: {
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
fromEmail?: string;
|
|
10
|
+
fromName?: string;
|
|
11
|
+
dailyQuota?: number;
|
|
12
|
+
};
|
|
13
|
+
brevo?: {
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
fromEmail?: string;
|
|
16
|
+
fromName?: string;
|
|
17
|
+
dailyQuota?: number;
|
|
18
|
+
};
|
|
19
|
+
smtp?: {
|
|
20
|
+
host?: string;
|
|
21
|
+
port?: number;
|
|
22
|
+
user?: string;
|
|
23
|
+
pass?: string;
|
|
24
|
+
fromEmail?: string;
|
|
25
|
+
fromName?: string;
|
|
26
|
+
secure?: boolean;
|
|
27
|
+
dailyQuota?: number;
|
|
28
|
+
};
|
|
29
|
+
fromEmail?: string;
|
|
30
|
+
fromName?: string;
|
|
31
|
+
healthCheck?: {
|
|
32
|
+
enabled?: boolean;
|
|
33
|
+
intervalMs?: number;
|
|
34
|
+
maxRetries?: number;
|
|
35
|
+
retryDelayMs?: number;
|
|
36
|
+
};
|
|
37
|
+
warmUpTimeout?: number;
|
|
38
|
+
sendTimeout?: number;
|
|
39
|
+
}
|
|
40
|
+
export declare const MAIL_DISABLED_MSG = "Mail service is not configured. Configure resend, brevo, or smtp provider.";
|
|
41
|
+
export declare class MailPlugin implements IPlugin, MailProvider {
|
|
42
|
+
readonly name = "app:mail";
|
|
43
|
+
readonly version = "0.0.1";
|
|
44
|
+
logger?: ILogger;
|
|
45
|
+
private providers;
|
|
46
|
+
private rateLimiters;
|
|
47
|
+
private readonly options;
|
|
48
|
+
private failedProviders;
|
|
49
|
+
private isWarming;
|
|
50
|
+
constructor(options?: MailPluginOptions);
|
|
51
|
+
getClient(name?: string): MailProvider | undefined;
|
|
52
|
+
warmUp(): Promise<void>;
|
|
53
|
+
send(options: SendMailOptions, providerName?: string): Promise<SendMailResult>;
|
|
54
|
+
healthCheck(): Promise<boolean>;
|
|
55
|
+
private tryInitResend;
|
|
56
|
+
private tryInitBrevo;
|
|
57
|
+
private tryInitSmtp;
|
|
58
|
+
beforeModulesRegistered(app: Application, _hono: unknown): Promise<void>;
|
|
59
|
+
afterModulesRegistered(_app: Application, _hono: unknown): Promise<void>;
|
|
60
|
+
runHealthChecks(): Promise<void>;
|
|
61
|
+
close(): Promise<void>;
|
|
62
|
+
private getCacheDriver;
|
|
63
|
+
private getEnv;
|
|
64
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { ENV_SYMBOL } from '@rx-ted/packages-core';
|
|
2
|
+
import { ComponentManager, resolvePluginLogger } from '@rx-ted/packages-honest';
|
|
3
|
+
import { RateLimiter } from './rate-limiter';
|
|
4
|
+
import { ResendMailProvider } from './providers/resend.provider';
|
|
5
|
+
import { BrevoMailProvider } from './providers/brevo.provider';
|
|
6
|
+
import { SmtpMailProvider } from './providers/smtp.provider';
|
|
7
|
+
export const MAIL_GLOBAL_KEY = 'app:mail';
|
|
8
|
+
export const MAIL_DISABLED_MSG = 'Mail service is not configured. Configure resend, brevo, or smtp provider.';
|
|
9
|
+
export class MailPlugin {
|
|
10
|
+
name = MAIL_GLOBAL_KEY;
|
|
11
|
+
version = '0.0.1';
|
|
12
|
+
logger;
|
|
13
|
+
providers = [];
|
|
14
|
+
rateLimiters = new Map();
|
|
15
|
+
options;
|
|
16
|
+
failedProviders = new Set();
|
|
17
|
+
isWarming = false;
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.options = options;
|
|
20
|
+
}
|
|
21
|
+
getClient(name) {
|
|
22
|
+
if (name)
|
|
23
|
+
return this.providers.find((p) => p.name === name);
|
|
24
|
+
return this.providers[0];
|
|
25
|
+
}
|
|
26
|
+
async warmUp() {
|
|
27
|
+
if (this.isWarming)
|
|
28
|
+
return;
|
|
29
|
+
this.isWarming = true;
|
|
30
|
+
const timeout = this.options.warmUpTimeout ?? 10_000;
|
|
31
|
+
const relevant = this.providers.filter((p) => typeof p.warmUp === 'function');
|
|
32
|
+
if (relevant.length === 0) {
|
|
33
|
+
this.isWarming = false;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const results = await Promise.allSettled(relevant.map((p) => (async () => {
|
|
37
|
+
try {
|
|
38
|
+
await Promise.race([
|
|
39
|
+
p.warmUp(),
|
|
40
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Warm-up timeout')), timeout)),
|
|
41
|
+
]);
|
|
42
|
+
this.logger?.info('[mail] Provider warmed up', { provider: p.name });
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
this.failedProviders.add(p.name);
|
|
46
|
+
this.logger?.warn('[mail] Provider warm-up failed or timed out', {
|
|
47
|
+
provider: p.name,
|
|
48
|
+
error: err instanceof Error ? err.message : String(err),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
})()));
|
|
52
|
+
const succeeded = results.filter((r) => r.status === 'fulfilled').length;
|
|
53
|
+
this.logger?.info('[mail] Warm-up completed', { total: relevant.length, succeeded });
|
|
54
|
+
this.isWarming = false;
|
|
55
|
+
}
|
|
56
|
+
async send(options, providerName) {
|
|
57
|
+
if (this.providers.length === 0) {
|
|
58
|
+
throw new Error(MAIL_DISABLED_MSG);
|
|
59
|
+
}
|
|
60
|
+
if (providerName) {
|
|
61
|
+
const provider = this.providers.find((p) => p.name === providerName);
|
|
62
|
+
if (!provider)
|
|
63
|
+
throw new Error(`Unknown provider "${providerName}"`);
|
|
64
|
+
if (this.failedProviders.has(providerName)) {
|
|
65
|
+
throw new Error(`Provider "${providerName}" unavailable (warm-up failed)`);
|
|
66
|
+
}
|
|
67
|
+
const limiter = this.rateLimiters.get(providerName);
|
|
68
|
+
if (limiter)
|
|
69
|
+
await limiter.checkAndIncrement();
|
|
70
|
+
return provider.send(options);
|
|
71
|
+
}
|
|
72
|
+
const errors = [];
|
|
73
|
+
for (const provider of this.providers) {
|
|
74
|
+
if (this.failedProviders.has(provider.name))
|
|
75
|
+
continue;
|
|
76
|
+
try {
|
|
77
|
+
const limiter = this.rateLimiters.get(provider.name);
|
|
78
|
+
if (limiter)
|
|
79
|
+
await limiter.checkAndIncrement();
|
|
80
|
+
return await provider.send(options);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
errors.push(err);
|
|
84
|
+
this.logger?.warn?.('Provider failed, trying next', {
|
|
85
|
+
provider: provider.name,
|
|
86
|
+
error: err,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`All providers failed: ${errors.map((e) => e.message).join('; ')}`);
|
|
91
|
+
}
|
|
92
|
+
healthCheck() {
|
|
93
|
+
if (this.providers.length === 0)
|
|
94
|
+
return Promise.resolve(false);
|
|
95
|
+
return this.providers[0].healthCheck();
|
|
96
|
+
}
|
|
97
|
+
tryInitResend(cfg, cacheDrv, appEnv) {
|
|
98
|
+
const apiKey = cfg.apiKey ?? appEnv?.get('MAIL_RESEND_API_KEY');
|
|
99
|
+
if (!apiKey)
|
|
100
|
+
return;
|
|
101
|
+
const fromEmail = cfg.fromEmail ??
|
|
102
|
+
appEnv?.var('MAIL_RESEND_FROM_EMAIL', 'noreply@19981204.xyz') ??
|
|
103
|
+
'noreply@19981204.xyz';
|
|
104
|
+
const fromName = cfg.fromName ?? appEnv?.var('MAIL_RESEND_FROM_NAME', 'noreply') ?? 'noreply';
|
|
105
|
+
this.providers.push(new ResendMailProvider('resend', { apiKey }, fromEmail, fromName));
|
|
106
|
+
this.rateLimiters.set('resend', new RateLimiter(cfg.dailyQuota ?? Infinity, 'resend', cacheDrv, this.logger));
|
|
107
|
+
this.logger?.info('Mail provider registered', { name: 'resend' });
|
|
108
|
+
}
|
|
109
|
+
tryInitBrevo(cfg, cacheDrv, appEnv) {
|
|
110
|
+
const apiKey = cfg.apiKey ?? appEnv?.get('MAIL_BREVO_API_KEY');
|
|
111
|
+
if (!apiKey)
|
|
112
|
+
return;
|
|
113
|
+
const fromEmail = cfg.fromEmail ??
|
|
114
|
+
appEnv?.var('MAIL_BREVO_FROM_EMAIL', 'noreply@19981204.xyz') ??
|
|
115
|
+
'noreply@19981204.xyz';
|
|
116
|
+
const fromName = cfg.fromName ?? appEnv?.var('MAIL_BREVO_FROM_NAME', 'noreply') ?? 'noreply';
|
|
117
|
+
this.providers.push(new BrevoMailProvider('brevo', { apiKey }, fromEmail, fromName));
|
|
118
|
+
this.rateLimiters.set('brevo', new RateLimiter(cfg.dailyQuota ?? Infinity, 'brevo', cacheDrv, this.logger));
|
|
119
|
+
this.logger?.info('Mail provider registered', { name: 'brevo' });
|
|
120
|
+
}
|
|
121
|
+
tryInitSmtp(cfg, cacheDrv, appEnv) {
|
|
122
|
+
const host = cfg.host ?? appEnv?.get('MAIL_SMTP_HOST');
|
|
123
|
+
const user = cfg.user ?? appEnv?.get('MAIL_SMTP_USER');
|
|
124
|
+
const pass = cfg.pass ?? appEnv?.get('MAIL_SMTP_PASS');
|
|
125
|
+
const port = cfg.port ?? appEnv?.get('MAIL_SMTP_PORT', 'number') ?? 587;
|
|
126
|
+
if (!host || !user || !pass)
|
|
127
|
+
return;
|
|
128
|
+
const fromEmail = (cfg.fromEmail ?? appEnv?.var('MAIL_SMTP_FROM_EMAIL')) || user;
|
|
129
|
+
const fromName = cfg.fromName ?? appEnv?.var('MAIL_SMTP_FROM_NAME', 'rx-ted') ?? 'rx-ted';
|
|
130
|
+
this.providers.push(new SmtpMailProvider('smtp', { host, port, user, pass, secure: cfg.secure }, fromEmail, fromName));
|
|
131
|
+
this.rateLimiters.set('smtp', new RateLimiter(cfg.dailyQuota ?? Infinity, 'smtp', cacheDrv, this.logger));
|
|
132
|
+
this.logger?.info('Mail provider registered', { name: 'smtp' });
|
|
133
|
+
}
|
|
134
|
+
async beforeModulesRegistered(app, _hono) {
|
|
135
|
+
this.logger ??= resolvePluginLogger(this.name);
|
|
136
|
+
const cacheDrv = this.getCacheDriver(app);
|
|
137
|
+
const appEnv = this.getEnv();
|
|
138
|
+
this.tryInitResend(this.options.resend ?? {}, cacheDrv, appEnv);
|
|
139
|
+
this.tryInitBrevo(this.options.brevo ?? {}, cacheDrv, appEnv);
|
|
140
|
+
this.tryInitSmtp(this.options.smtp ?? {}, cacheDrv, appEnv);
|
|
141
|
+
if (this.providers.length === 0) {
|
|
142
|
+
this.logger.warn(MAIL_DISABLED_MSG);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
for (const p of this.providers) {
|
|
146
|
+
ComponentManager.registerPlugin(`mail:${p.name}`, p);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
ComponentManager.registerPlugin(MAIL_GLOBAL_KEY, this);
|
|
150
|
+
}
|
|
151
|
+
async afterModulesRegistered(_app, _hono) {
|
|
152
|
+
if (this.providers.length === 0 || !this.logger)
|
|
153
|
+
return;
|
|
154
|
+
if (this.options.healthCheck?.enabled === false) {
|
|
155
|
+
this.logger.info('Health check disabled by config');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
this.logger.info('Health check deferred to background (will run via waitUntil)');
|
|
159
|
+
}
|
|
160
|
+
async runHealthChecks() {
|
|
161
|
+
if (this.providers.length === 0 || !this.logger)
|
|
162
|
+
return;
|
|
163
|
+
if (this.options.healthCheck?.enabled === false)
|
|
164
|
+
return;
|
|
165
|
+
const maxRetries = this.options.healthCheck?.maxRetries ?? 3;
|
|
166
|
+
const retryDelay = this.options.healthCheck?.retryDelayMs ?? 1000;
|
|
167
|
+
for (const provider of this.providers) {
|
|
168
|
+
const limiter = this.rateLimiters.get(provider.name);
|
|
169
|
+
if (!limiter)
|
|
170
|
+
continue;
|
|
171
|
+
const done = await limiter.wasHealthCheckDoneToday();
|
|
172
|
+
if (done) {
|
|
173
|
+
this.logger.info('Health check already done today', { provider: provider.name });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
let success = false;
|
|
177
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
178
|
+
this.logger.info('Running health check', { provider: provider.name, attempt });
|
|
179
|
+
success = await provider.healthCheck();
|
|
180
|
+
if (success)
|
|
181
|
+
break;
|
|
182
|
+
if (attempt < maxRetries)
|
|
183
|
+
await sleep(retryDelay);
|
|
184
|
+
}
|
|
185
|
+
if (success) {
|
|
186
|
+
await limiter.markHealthCheckDone();
|
|
187
|
+
this.logger.info('Health check passed', { provider: provider.name });
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
this.logger.warn('Health check failed after retries', {
|
|
191
|
+
provider: provider.name,
|
|
192
|
+
maxRetries,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async close() {
|
|
198
|
+
for (const provider of this.providers) {
|
|
199
|
+
try {
|
|
200
|
+
await provider.close();
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
this.logger?.error?.('Error closing provider', { provider: provider.name, error: e });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
getCacheDriver(_app) {
|
|
208
|
+
try {
|
|
209
|
+
if (ComponentManager.hasPlugin('cache')) {
|
|
210
|
+
return ComponentManager.getPlugin('cache');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
getEnv() {
|
|
219
|
+
try {
|
|
220
|
+
if (ComponentManager.hasPlugin(ENV_SYMBOL)) {
|
|
221
|
+
return ComponentManager.getPlugin(ENV_SYMBOL);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function sleep(ms) {
|
|
231
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
232
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MailProvider, SendMailOptions, SendMailResult } from '../types';
|
|
2
|
+
export declare class BrevoMailProvider implements MailProvider {
|
|
3
|
+
private config;
|
|
4
|
+
private fromEmail;
|
|
5
|
+
private fromName?;
|
|
6
|
+
readonly name: string;
|
|
7
|
+
private client;
|
|
8
|
+
constructor(name: string, config: {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
}, fromEmail: string, fromName?: string | undefined);
|
|
11
|
+
private ensureInit;
|
|
12
|
+
send(options: SendMailOptions, _providerName?: string): Promise<SendMailResult>;
|
|
13
|
+
healthCheck(): Promise<boolean>;
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { BrevoClient } from '@getbrevo/brevo';
|
|
2
|
+
export class BrevoMailProvider {
|
|
3
|
+
config;
|
|
4
|
+
fromEmail;
|
|
5
|
+
fromName;
|
|
6
|
+
name;
|
|
7
|
+
client = null;
|
|
8
|
+
constructor(name, config, fromEmail, fromName) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.fromEmail = fromEmail;
|
|
11
|
+
this.fromName = fromName;
|
|
12
|
+
this.name = name;
|
|
13
|
+
}
|
|
14
|
+
async ensureInit() {
|
|
15
|
+
if (this.client)
|
|
16
|
+
return;
|
|
17
|
+
this.client = new BrevoClient({ apiKey: this.config.apiKey });
|
|
18
|
+
}
|
|
19
|
+
async send(options, _providerName) {
|
|
20
|
+
await this.ensureInit();
|
|
21
|
+
const to = (Array.isArray(options.to) ? options.to : [options.to]).map((e) => ({
|
|
22
|
+
email: e,
|
|
23
|
+
}));
|
|
24
|
+
const payload = {
|
|
25
|
+
sender: { name: this.fromName ?? '', email: options.from || this.fromEmail },
|
|
26
|
+
to,
|
|
27
|
+
subject: options.subject,
|
|
28
|
+
};
|
|
29
|
+
if (options.html)
|
|
30
|
+
payload.htmlContent = options.html;
|
|
31
|
+
if (options.text)
|
|
32
|
+
payload.textContent = options.text;
|
|
33
|
+
if (options.cc)
|
|
34
|
+
payload.cc = (Array.isArray(options.cc) ? options.cc : [options.cc]).map((e) => ({
|
|
35
|
+
email: e,
|
|
36
|
+
}));
|
|
37
|
+
if (options.bcc)
|
|
38
|
+
payload.bcc = (Array.isArray(options.bcc) ? options.bcc : [options.bcc]).map((e) => ({
|
|
39
|
+
email: e,
|
|
40
|
+
}));
|
|
41
|
+
if (options.attachments?.length) {
|
|
42
|
+
payload.attachment = options.attachments.map((a) => ({
|
|
43
|
+
name: a.filename,
|
|
44
|
+
content: a.content instanceof Uint8Array ? uint8ArrayToBase64(a.content) : a.content,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
const result = await this.client.transactionalEmails.sendTransacEmail(payload);
|
|
48
|
+
return { id: result?.messageId ?? 'unknown', provider: this.name };
|
|
49
|
+
}
|
|
50
|
+
async healthCheck() {
|
|
51
|
+
try {
|
|
52
|
+
await this.ensureInit();
|
|
53
|
+
await this.client.transactionalEmails.sendTransacEmail({
|
|
54
|
+
sender: { name: this.fromName ?? '', email: this.fromEmail },
|
|
55
|
+
to: [{ email: this.fromEmail }],
|
|
56
|
+
subject: 'Health check',
|
|
57
|
+
textContent: 'This is a health check email from Brevo provider.',
|
|
58
|
+
});
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async close() {
|
|
66
|
+
this.client = null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function uint8ArrayToBase64(u8) {
|
|
70
|
+
let binary = '';
|
|
71
|
+
for (let i = 0; i < u8.length; i++) {
|
|
72
|
+
binary += String.fromCharCode(u8[i]);
|
|
73
|
+
}
|
|
74
|
+
return btoa(binary);
|
|
75
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MailProvider, SendMailOptions, SendMailResult } from '../types';
|
|
2
|
+
export declare class CustomMailProvider implements MailProvider {
|
|
3
|
+
private impl;
|
|
4
|
+
readonly name: string;
|
|
5
|
+
constructor(name: string, impl: MailProvider);
|
|
6
|
+
send(options: SendMailOptions, providerName?: string): Promise<SendMailResult>;
|
|
7
|
+
healthCheck(): Promise<boolean>;
|
|
8
|
+
close(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class CustomMailProvider {
|
|
2
|
+
impl;
|
|
3
|
+
name;
|
|
4
|
+
constructor(name, impl) {
|
|
5
|
+
this.impl = impl;
|
|
6
|
+
this.name = name;
|
|
7
|
+
}
|
|
8
|
+
async send(options, providerName) {
|
|
9
|
+
return this.impl.send(options, providerName);
|
|
10
|
+
}
|
|
11
|
+
async healthCheck() {
|
|
12
|
+
if (typeof this.impl.healthCheck === 'function') {
|
|
13
|
+
return this.impl.healthCheck();
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
async close() {
|
|
18
|
+
if (typeof this.impl.close === 'function') {
|
|
19
|
+
await this.impl.close();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|