@t4h.framework/smtp 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: framework-smtp
|
|
3
|
+
description: >-
|
|
4
|
+
Guides correct use of @t4h.framework/smtp in T4H Framework workflows: SMTP
|
|
5
|
+
send-email activity (SmtpSendEmailActivity), SmtpSendEmailClaim, reconciler-bound
|
|
6
|
+
Smtp client, recipients/cc/bcc normalization, HTML bodies, Binary attachments,
|
|
7
|
+
and SmtpSendEmailOutput. Use when sending email from workflows, wiring SMTP
|
|
8
|
+
claim providers, testing email activities, implementing SmtpSendEmailClaim,
|
|
9
|
+
or working in packages/smtp.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# @t4h.framework/smtp
|
|
13
|
+
|
|
14
|
+
SMTP activity, claim, and a reconciler-aware client for sending email inside workflows. Package path: `framework/packages/smtp`. Peer dependency: `@t4h.framework/core` (for `SyncActivity`, `Claim`, `History`, `Binary`, `SerializableClass`).
|
|
15
|
+
|
|
16
|
+
Import surface (from `src/smtp.ts` only — do not reference unexported modules):
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import {
|
|
20
|
+
Smtp,
|
|
21
|
+
SmtpSendEmailActivity,
|
|
22
|
+
SmtpSendEmailClaim,
|
|
23
|
+
SmtpSendEmailOutput,
|
|
24
|
+
type SmtpOptions,
|
|
25
|
+
type SmtpSendEmailActivityOptions,
|
|
26
|
+
type SmtpSendEmailActivityInput,
|
|
27
|
+
type SmtpSendEmailActivityAttachment,
|
|
28
|
+
type ISmtpSendEmailOutput,
|
|
29
|
+
} from '@t4h.framework/smtp'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Node `>=22`. See [README.md](../../../README.md) for a short overview (note: README may say `sendEmail` on the claim; the actual method is **`send`**).
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Architecture
|
|
37
|
+
|
|
38
|
+
`Workflow` → `Smtp.send` (`History.bind(SmtpSendEmailActivity)`) → `SmtpSendEmailActivity.toInput` / `run` → `SmtpSendEmailClaim.send` → `SmtpSendEmailOutput`.
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
┌─────────────┐ send() ┌────────────────────────┐ run() ┌─────────────────────┐
|
|
42
|
+
│ Smtp │ ──────────────► │ SmtpSendEmailActivity │ ────────────► │ SmtpSendEmailClaim │
|
|
43
|
+
│ (client) │ reconciler │ (SyncActivity) │ Claim.smtp │ (runtime impl) │
|
|
44
|
+
└─────────────┘ └────────────────────────┘ └─────────────────────┘
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Never call nodemailer or raw SMTP sockets directly in workflow code if the step must be replayable — use `Smtp` / `SmtpSendEmailActivity`.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## SmtpSendEmailClaim (required for delivery)
|
|
52
|
+
|
|
53
|
+
Abstract dependency; the **runtime** must register a concrete implementation:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
{ provide: SmtpSendEmailClaim, value: new MySmtpSendEmailClaimImpl() }
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Contract (from source):
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
abstract class SmtpSendEmailClaim {
|
|
63
|
+
abstract send(
|
|
64
|
+
input: SmtpSendEmailActivityInput,
|
|
65
|
+
): ISmtpSendEmailOutput | Promise<ISmtpSendEmailOutput>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type ISmtpSendEmailOutput = { messageId: string }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`SmtpSendEmailActivity.run` passes the full **`SmtpSendEmailActivityInput`** payload to `claim.send` (sync or async).
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Send options and payload
|
|
76
|
+
|
|
77
|
+
The exported `SmtpSendEmailActivity` — driven by `smtp.send` or directly via
|
|
78
|
+
`History.reconciler(SmtpSendEmailActivity, smtp, content)` — is replay-safe and
|
|
79
|
+
delegates to `SmtpSendEmailClaim.send`, returning a `SmtpSendEmailOutput`. Its
|
|
80
|
+
input shapes:
|
|
81
|
+
|
|
82
|
+
### SmtpSendEmailActivityOptions (what `smtp.send` accepts)
|
|
83
|
+
|
|
84
|
+
| Field | Type | Required | Notes |
|
|
85
|
+
|-------|------|----------|-------|
|
|
86
|
+
| `to` | `string \| readonly string[]` | yes | Normalized to `readonly string[]` in input |
|
|
87
|
+
| `subject` | `string` | yes | |
|
|
88
|
+
| `html` | `string` | yes | HTML body |
|
|
89
|
+
| `from` | `string?` | no | Overrides `smtp.options.from` |
|
|
90
|
+
| `cc` | `string \| readonly string[]?` | no | String → single-element array |
|
|
91
|
+
| `bcc` | `string \| readonly string[]?` | no | String → single-element array |
|
|
92
|
+
| `attachments` | `readonly SmtpSendEmailActivityAttachment[]?` | no | Passed through unchanged |
|
|
93
|
+
|
|
94
|
+
### SmtpSendEmailActivityInput (claim / activity payload)
|
|
95
|
+
|
|
96
|
+
| Field | Type | Notes |
|
|
97
|
+
|-------|------|-------|
|
|
98
|
+
| `connectionString` | `string` | From `smtp.connectionString` |
|
|
99
|
+
| `from` | `string?` | `content.from ?? smtp.options.from` |
|
|
100
|
+
| `to` | `readonly string[]` | Always an array after `toInput` |
|
|
101
|
+
| `cc` | `readonly string[]?` | Omitted if not set |
|
|
102
|
+
| `bcc` | `readonly string[]?` | Omitted if not set |
|
|
103
|
+
| `subject` | `string` | |
|
|
104
|
+
| `html` | `string` | |
|
|
105
|
+
| `attachments` | `readonly SmtpSendEmailActivityAttachment[]?` | |
|
|
106
|
+
|
|
107
|
+
### Attachments
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import type { Binary } from '@t4h.framework/core'
|
|
111
|
+
|
|
112
|
+
type SmtpSendEmailActivityAttachment = {
|
|
113
|
+
filename: string
|
|
114
|
+
content: Binary
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Use framework `Binary` instances (e.g. from an HTTP `response.body`).
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Smtp client
|
|
123
|
+
|
|
124
|
+
High-level API: holds `connectionString` and optional default `from`, extends `SerializableClass` for workflow persistence.
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
const smtp = new Smtp('smtp://user:pass@smtp.example.com:587', {
|
|
128
|
+
from: 'noreply@example.com',
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const result = await smtp.send({
|
|
132
|
+
to: 'recipient@example.com',
|
|
133
|
+
subject: 'Hello',
|
|
134
|
+
html: '<p>Hello world</p>',
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
result.messageId // string — via SmtpSendEmailOutput
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- `send(options)` returns a promise of `SmtpSendEmailOutput` when history/reconciler completes (same pattern as `http` client).
|
|
141
|
+
- Default `from`: set on `Smtp` constructor options; per-send `from` in `send()` wins over `smtp.options.from`.
|
|
142
|
+
- `toSerializable()` / `fromSerializable()` restore `connectionString` and `options` (see `Smtp.spec.ts`).
|
|
143
|
+
|
|
144
|
+
### Multiple recipients, cc, bcc
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
await smtp.send({
|
|
148
|
+
to: ['alice@example.com', 'bob@example.com'],
|
|
149
|
+
cc: 'manager@example.com',
|
|
150
|
+
bcc: ['audit@example.com'],
|
|
151
|
+
subject: 'Team Update',
|
|
152
|
+
html: '<p>Weekly report.</p>',
|
|
153
|
+
})
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Using the exported activity directly via `History.reconciler`
|
|
157
|
+
|
|
158
|
+
Prefer `smtp.send` in workflows. When you need the exported activity directly,
|
|
159
|
+
drive the **shipped** `SmtpSendEmailActivity` through `History.reconciler` — a
|
|
160
|
+
ready, replay-safe helper. Never define or extend your own activity:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import { History } from '@t4h.framework/core'
|
|
164
|
+
import { Smtp, SmtpSendEmailActivity } from '@t4h.framework/smtp'
|
|
165
|
+
|
|
166
|
+
const smtp = new Smtp(connectionString)
|
|
167
|
+
const result = await History.reconciler(SmtpSendEmailActivity, smtp, {
|
|
168
|
+
to: 'user@example.com',
|
|
169
|
+
subject: 'Subject',
|
|
170
|
+
html: '<p>Body</p>',
|
|
171
|
+
})
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Runs inside a workflow (the runtime, or `TestWorkflowEnvironment` in tests, provides the reconciler context).
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## SmtpSendEmailOutput
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
class SmtpSendEmailOutput {
|
|
182
|
+
constructor(public readonly raw: ISmtpSendEmailOutput)
|
|
183
|
+
get messageId(): string
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Activity and `Smtp.send` surface this type, not the raw `{ messageId }` object.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Workflow example
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { App, EnvDesign, Workflow } from '@t4h.framework/core'
|
|
195
|
+
import { http } from '@t4h.framework/http'
|
|
196
|
+
import { Smtp } from '@t4h.framework/smtp'
|
|
197
|
+
import Type from 'typebox'
|
|
198
|
+
|
|
199
|
+
const env = EnvDesign({
|
|
200
|
+
SMTP_CONNECTION_STRING: { type: String },
|
|
201
|
+
SMTP_FROM: { type: String },
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const smtp = new Smtp(env.SMTP_CONNECTION_STRING)
|
|
205
|
+
|
|
206
|
+
export default new App({
|
|
207
|
+
id: 'email',
|
|
208
|
+
workflows: [
|
|
209
|
+
new Workflow({ id: 'send-email', schema: Type.String() }, async input => {
|
|
210
|
+
const response = await http.get('https://picsum.photos/300/200')
|
|
211
|
+
|
|
212
|
+
await smtp.send({
|
|
213
|
+
from: env.SMTP_FROM,
|
|
214
|
+
to: input,
|
|
215
|
+
subject: 'Hello World!',
|
|
216
|
+
html: '<p>...</p>',
|
|
217
|
+
attachments: [{ filename: 'image.png', content: response.body.binary }],
|
|
218
|
+
})
|
|
219
|
+
}),
|
|
220
|
+
],
|
|
221
|
+
})
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Connection strings are opaque to the package; dev implementation expects URL form with credentials in the authority (e.g. `smtp://user:pass@host:587`, `smtps://...`).
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Testing
|
|
229
|
+
|
|
230
|
+
Run package tests: `yarn test` in `packages/smtp` (Vitest).
|
|
231
|
+
|
|
232
|
+
Test the **workflow** with `TestWorkflowEnvironment` from
|
|
233
|
+
`@t4h.framework/core/testing`, mocking `SmtpSendEmailClaim` — the single claim
|
|
234
|
+
the send activity resolves. See the **framework-workflow-testing** skill for the
|
|
235
|
+
harness API.
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { TestWorkflowEnvironment, mockClaim } from '@t4h.framework/core/testing'
|
|
239
|
+
import { SmtpSendEmailClaim } from '@t4h.framework/smtp'
|
|
240
|
+
|
|
241
|
+
const mockSend = vi.fn().mockImplementation(async () => ({ messageId: 'msg-123' }))
|
|
242
|
+
|
|
243
|
+
const env = TestWorkflowEnvironment.create({ now: 0 })
|
|
244
|
+
|
|
245
|
+
await env.execute(workflow, input, {
|
|
246
|
+
claims: [mockClaim(SmtpSendEmailClaim, { send: mockSend })],
|
|
247
|
+
env: { SMTP_CONNECTION_STRING: 'smtp://user:pass@smtp.example.com:587' },
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// `send` receives the normalized activity input (single `to` becomes an array, etc.)
|
|
251
|
+
expect(mockSend.mock.calls[0][0]).toMatchObject({
|
|
252
|
+
to: ['recipient@example.com'],
|
|
253
|
+
subject: 'Test',
|
|
254
|
+
html: '<p>Hello</p>',
|
|
255
|
+
})
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Implementing SmtpSendEmailClaim
|
|
261
|
+
|
|
262
|
+
1. Subclass `SmtpSendEmailClaim` and implement **`send(input: SmtpSendEmailActivityInput)`**.
|
|
263
|
+
2. Read `connectionString`, `from`, `to`, `cc`, `bcc`, `subject`, `html`, `attachments` from `input` (arrays already normalized).
|
|
264
|
+
3. Return `{ messageId: string }` (or a Promise thereof). `messageId` is what workflows expose via `SmtpSendEmailOutput.messageId`.
|
|
265
|
+
4. For attachments, consume `Binary` via `.stream()`, `.contentType`, etc.
|
|
266
|
+
5. Register: `{ provide: SmtpSendEmailClaim, value: new MyClaim() }`.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Checklist for agents
|
|
271
|
+
|
|
272
|
+
1. In workflows, use **`Smtp` + `smtp.send()`** (or `History.reconciler` with the exported `SmtpSendEmailActivity`) — never define or extend your own activity.
|
|
273
|
+
2. Ensure runtime provides **`SmtpSendEmailClaim`** (or a mock in tests).
|
|
274
|
+
3. Use **`html`** for the message body; required fields on send options are **`to`**, **`subject`**, **`html`**.
|
|
275
|
+
4. Set default **`from`** on `new Smtp(connectionString, { from })` or per-send; per-send wins.
|
|
276
|
+
5. Pass **`Binary`** for attachment `content`, not raw `Buffer` in activity options.
|
|
277
|
+
6. Expect **`SmtpSendEmailOutput`** from `send`, not a plain object — use `.messageId`.
|
|
278
|
+
7. Test workflows with **`TestWorkflowEnvironment`** + **`mockClaim(SmtpSendEmailClaim, { send })`**; assert on the mock's `mock.calls`.
|
|
279
|
+
8. Do not document or import APIs not exported from `@t4h.framework/smtp`.
|
|
280
|
+
9. For core workflow/reconciler concepts, use the **framework-core** skill (`@t4h.framework/core`).
|
|
281
|
+
|
|
282
|
+
See also: `packages/smtp/README.md`; **framework-workflow-testing** for the test harness.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAA;AAEpE,wBAAgB,aAAa,CAC3B,cAAc,GAAE,OAAO,CAAC,kBAAkB,CAAM,mEAMjD"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { mockClaim } from '@t4h.framework/core/testing';
|
|
2
|
+
import { SmtpSendEmailClaim } from '../models/SmtpSendEmailClaim.js';
|
|
3
|
+
export function mockSmtpClaim(implementation = {}) {
|
|
4
|
+
return mockClaim(SmtpSendEmailClaim, {
|
|
5
|
+
send: async () => ({ messageId: 'mock-message-id' }),
|
|
6
|
+
...implementation,
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAA;AAEvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAA;AAEpE,MAAM,UAAU,aAAa,CAC3B,iBAA8C,EAAE;IAEhD,OAAO,SAAS,CAAC,kBAAkB,EAAE;QACnC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,iBAAiB,EAAE,CAAC;QACpD,GAAG,cAAc;KAClB,CAAC,CAAA;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@t4h.framework/smtp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "SMTP module for the T4H Framework",
|
|
5
5
|
"homepage": "https://github.com/tech4humans-brasil/framework/tree/main/packages/smtp",
|
|
6
6
|
"bugs": "https://github.com/tech4humans-brasil/framework/issues",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"types": "./dist/smtp.d.ts",
|
|
21
21
|
"files": [
|
|
22
22
|
"dist",
|
|
23
|
-
"LICENSE"
|
|
23
|
+
"LICENSE",
|
|
24
|
+
".ai"
|
|
24
25
|
],
|
|
25
26
|
"scripts": {
|
|
26
27
|
"build": "tsc --project tsconfig.build.json",
|
|
@@ -29,15 +30,13 @@
|
|
|
29
30
|
"test": "vitest run",
|
|
30
31
|
"test:watch": "vitest"
|
|
31
32
|
},
|
|
32
|
-
"dependencies": {
|
|
33
|
-
"@t4h.framework/core": "^0.3.0"
|
|
34
|
-
},
|
|
35
33
|
"devDependencies": {
|
|
34
|
+
"@t4h.framework/core": "^0.8.0",
|
|
36
35
|
"typescript": "^5.9.3",
|
|
37
36
|
"vitest": "^4.0.18"
|
|
38
37
|
},
|
|
39
38
|
"peerDependencies": {
|
|
40
|
-
"@t4h.framework/core": "^0.
|
|
39
|
+
"@t4h.framework/core": "^0.8.0"
|
|
41
40
|
},
|
|
42
41
|
"packageManager": "yarn@4.12.0",
|
|
43
42
|
"engines": {
|