@t4h.framework/smtp 0.3.0 → 0.4.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,354 @@
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 or examples/email.
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
+ ## SmtpSendEmailActivity
76
+
77
+ `SyncActivity` with args `[smtp: Smtp, content: SmtpSendEmailActivityOptions]`.
78
+
79
+ | Method | Role |
80
+ |--------|------|
81
+ | `toInput(smtp, content)` | Builds serializable `SmtpSendEmailActivityInput` |
82
+ | `run(payload)` | `this.claims.smtp.send(payload)` |
83
+ | `toOutput(raw)` | `new SmtpSendEmailOutput(raw)` |
84
+
85
+ Claim wiring (private `Claim` with key `smtp` → `SmtpSendEmailClaim`).
86
+
87
+ ### SmtpSendEmailActivityOptions (designer / `Smtp.send`)
88
+
89
+ | Field | Type | Required | Notes |
90
+ |-------|------|----------|-------|
91
+ | `to` | `string \| readonly string[]` | yes | Normalized to `readonly string[]` in input |
92
+ | `subject` | `string` | yes | |
93
+ | `html` | `string` | yes | HTML body |
94
+ | `from` | `string?` | no | Overrides `smtp.options.from` |
95
+ | `cc` | `string \| readonly string[]?` | no | String → single-element array |
96
+ | `bcc` | `string \| readonly string[]?` | no | String → single-element array |
97
+ | `attachments` | `readonly SmtpSendEmailActivityAttachment[]?` | no | Passed through unchanged |
98
+
99
+ ### SmtpSendEmailActivityInput (claim / activity payload)
100
+
101
+ | Field | Type | Notes |
102
+ |-------|------|-------|
103
+ | `connectionString` | `string` | From `smtp.connectionString` |
104
+ | `from` | `string?` | `content.from ?? smtp.options.from` |
105
+ | `to` | `readonly string[]` | Always an array after `toInput` |
106
+ | `cc` | `readonly string[]?` | Omitted if not set |
107
+ | `bcc` | `readonly string[]?` | Omitted if not set |
108
+ | `subject` | `string` | |
109
+ | `html` | `string` | |
110
+ | `attachments` | `readonly SmtpSendEmailActivityAttachment[]?` | |
111
+
112
+ ### Attachments
113
+
114
+ ```typescript
115
+ import type { Binary } from '@t4h.framework/core'
116
+
117
+ type SmtpSendEmailActivityAttachment = {
118
+ filename: string
119
+ content: Binary
120
+ }
121
+ ```
122
+
123
+ Use framework `Binary` instances (e.g. from HTTP `response.body.binary` in `examples/email`).
124
+
125
+ ---
126
+
127
+ ## Smtp client
128
+
129
+ High-level API: holds `connectionString` and optional default `from`, extends `SerializableClass` for workflow persistence.
130
+
131
+ ```typescript
132
+ const smtp = new Smtp('smtp://user:pass@smtp.example.com:587', {
133
+ from: 'noreply@example.com',
134
+ })
135
+
136
+ const result = await smtp.send({
137
+ to: 'recipient@example.com',
138
+ subject: 'Hello',
139
+ html: '<p>Hello world</p>',
140
+ })
141
+
142
+ result.messageId // string — via SmtpSendEmailOutput
143
+ ```
144
+
145
+ - `send(options)` returns a promise of `SmtpSendEmailOutput` when history/reconciler completes (same pattern as `http` client).
146
+ - Default `from`: set on `Smtp` constructor options; per-send `from` in `send()` wins over `smtp.options.from`.
147
+ - `toSerializable()` / `fromSerializable()` restore `connectionString` and `options` (see `Smtp.spec.ts`).
148
+
149
+ ### Multiple recipients, cc, bcc
150
+
151
+ ```typescript
152
+ await smtp.send({
153
+ to: ['alice@example.com', 'bob@example.com'],
154
+ cc: 'manager@example.com',
155
+ bcc: ['audit@example.com'],
156
+ subject: 'Team Update',
157
+ html: '<p>Weekly report.</p>',
158
+ })
159
+ ```
160
+
161
+ ### Invoking the activity directly
162
+
163
+ Prefer `smtp.send` in workflows. For low-level control:
164
+
165
+ ```typescript
166
+ import { History } from '@t4h.framework/core'
167
+ import { Smtp, SmtpSendEmailActivity } from '@t4h.framework/smtp'
168
+
169
+ const smtp = new Smtp(connectionString)
170
+ const result = await History.reconciler(SmtpSendEmailActivity, smtp, {
171
+ to: 'user@example.com',
172
+ subject: 'Subject',
173
+ html: '<p>Body</p>',
174
+ })
175
+ ```
176
+
177
+ Must run inside `History.run` (workflow runtime or test harness).
178
+
179
+ ---
180
+
181
+ ## SmtpSendEmailOutput
182
+
183
+ ```typescript
184
+ class SmtpSendEmailOutput {
185
+ constructor(public readonly raw: ISmtpSendEmailOutput)
186
+ get messageId(): string
187
+ }
188
+ ```
189
+
190
+ Activity and `Smtp.send` surface this type, not the raw `{ messageId }` object.
191
+
192
+ ---
193
+
194
+ ## Workflow example
195
+
196
+ From `examples/email/src/main.tsx`:
197
+
198
+ ```typescript
199
+ import { App, EnvDesign, Workflow } from '@t4h.framework/core'
200
+ import { http } from '@t4h.framework/http'
201
+ import { Smtp } from '@t4h.framework/smtp'
202
+ import Type from 'typebox'
203
+
204
+ const env = EnvDesign({
205
+ SMTP_CONNECTION_STRING: { type: String },
206
+ SMTP_FROM: { type: String },
207
+ })
208
+
209
+ const smtp = new Smtp(env.SMTP_CONNECTION_STRING)
210
+
211
+ export default new App({
212
+ id: 'email',
213
+ workflows: [
214
+ new Workflow({ id: 'send-email', schema: Type.String() }, async input => {
215
+ const response = await http.get('https://picsum.photos/300/200')
216
+
217
+ await smtp.send({
218
+ from: env.SMTP_FROM,
219
+ to: input,
220
+ subject: 'Hello World!',
221
+ html: '<p>...</p>',
222
+ attachments: [{ filename: 'image.png', content: response.body.binary }],
223
+ })
224
+ }),
225
+ ],
226
+ })
227
+ ```
228
+
229
+ 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://...`).
230
+
231
+ ---
232
+
233
+ ## Testing
234
+
235
+ Run package tests: `yarn test` in `packages/smtp` (Vitest).
236
+
237
+ ### 1. Unit-test `toInput` / `run` / `toOutput`
238
+
239
+ Pattern from `SmtpSendEmailActivity.spec.ts`:
240
+
241
+ ```typescript
242
+ import { Claim } from '@t4h.framework/core'
243
+ import { vi } from 'vitest'
244
+ import {
245
+ Smtp,
246
+ SmtpSendEmailActivity,
247
+ SmtpSendEmailClaim,
248
+ type SmtpSendEmailActivityInput,
249
+ } from '@t4h.framework/smtp'
250
+
251
+ const mockSend = vi.fn().mockReturnValue({ messageId: 'msg-123' })
252
+
253
+ Claim.run(
254
+ [{ provide: SmtpSendEmailClaim, value: { send: mockSend } as SmtpSendEmailClaim }],
255
+ () => {
256
+ const activity = Reflect.construct(SmtpSendEmailActivity, [])
257
+ const input: SmtpSendEmailActivityInput = {
258
+ connectionString: 'smtp://user:pass@smtp.example.com:587',
259
+ from: 'sender@example.com',
260
+ subject: 'Test',
261
+ html: '<p>Hello</p>',
262
+ to: ['recipient@example.com'],
263
+ }
264
+ const output = activity.run(input)
265
+ expect(mockSend).toHaveBeenCalledWith(input)
266
+ expect(output).toEqual({ messageId: 'msg-123' })
267
+ },
268
+ )
269
+ ```
270
+
271
+ Use `mockResolvedValue` when testing async `send` implementations.
272
+
273
+ Assert `toInput` normalization: single `to` → array; `content.from` over `smtp.options.from`; `cc`/`bcc` string → array; attachments reference equality.
274
+
275
+ ### 2. Assert `Smtp.send` schedules `SmtpSendEmailActivity`
276
+
277
+ Pattern from `Smtp.spec.ts`:
278
+
279
+ ```typescript
280
+ import { History, PushActivity, type Activity } from '@t4h.framework/core'
281
+ import { Smtp, SmtpSendEmailActivity } from '@t4h.framework/smtp'
282
+
283
+ async function captureReconcilerTrigger(fn: () => unknown) {
284
+ try {
285
+ await History.run([], fn)
286
+ } catch (error) {
287
+ if (error instanceof PushActivity) return error as PushActivity<Activity>
288
+ throw error
289
+ }
290
+ }
291
+
292
+ const client = new Smtp(connectionString, { from: 'noreply@example.com' })
293
+ const trigger = await captureReconcilerTrigger(() =>
294
+ client.send({ to: 'a@example.com', subject: 'Test', html: '<p>Hi</p>' }),
295
+ )
296
+
297
+ expect(trigger.activity.constructor).toBe(SmtpSendEmailActivity)
298
+ expect(trigger.input).toEqual({
299
+ connectionString,
300
+ from: 'noreply@example.com',
301
+ subject: 'Test',
302
+ html: '<p>Hi</p>',
303
+ to: ['a@example.com'],
304
+ cc: undefined,
305
+ bcc: undefined,
306
+ attachments: undefined,
307
+ })
308
+ ```
309
+
310
+ ### 3. Replay with history
311
+
312
+ ```typescript
313
+ import { History } from '@t4h.framework/core'
314
+ import { Smtp, SmtpSendEmailOutput } from '@t4h.framework/smtp'
315
+
316
+ const result = await History.run(
317
+ [{ output: { messageId: '<msg-123@example.com>' }, type: 'sync' }],
318
+ () =>
319
+ client.send({
320
+ to: 'user@example.com',
321
+ subject: 'Subject',
322
+ html: '<p>Body</p>',
323
+ }),
324
+ )
325
+
326
+ expect(result).toBeInstanceOf(SmtpSendEmailOutput)
327
+ expect(result.messageId).toBe('<msg-123@example.com>')
328
+ ```
329
+
330
+ ---
331
+
332
+ ## Implementing SmtpSendEmailClaim
333
+
334
+ 1. Subclass `SmtpSendEmailClaim` and implement **`send(input: SmtpSendEmailActivityInput)`**.
335
+ 2. Read `connectionString`, `from`, `to`, `cc`, `bcc`, `subject`, `html`, `attachments` from `input` (arrays already normalized).
336
+ 3. Return `{ messageId: string }` (or a Promise thereof). `messageId` is what workflows expose via `SmtpSendEmailOutput.messageId`.
337
+ 4. For attachments, consume `Binary` via `.stream()`, `.contentType`, etc.
338
+ 5. Register: `{ provide: SmtpSendEmailClaim, value: new MyClaim() }`.
339
+
340
+ ---
341
+
342
+ ## Checklist for agents
343
+
344
+ 1. Prefer **`Smtp` + `smtp.send()`** in workflows unless a custom typed activity is required.
345
+ 2. Ensure runtime provides **`SmtpSendEmailClaim`** (or a mock in tests).
346
+ 3. Use **`html`** for the message body; required fields on send options are **`to`**, **`subject`**, **`html`**.
347
+ 4. Set default **`from`** on `new Smtp(connectionString, { from })` or per-send; per-send wins.
348
+ 5. Pass **`Binary`** for attachment `content`, not raw `Buffer` in activity options.
349
+ 6. Expect **`SmtpSendEmailOutput`** from `send`, not a plain object — use `.messageId`.
350
+ 7. When testing, mirror **`Claim.run`** + mock `send`, or **`History.run`** + **`PushActivity`** from `packages/smtp/src/**/__tests__`.
351
+ 8. Do not document or import APIs not exported from `@t4h.framework/smtp`.
352
+ 9. For core workflow/reconciler concepts, use the **framework-core** skill (`@t4h.framework/core`).
353
+
354
+ See also: `packages/smtp/README.md`, `examples/email/src/main.tsx`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t4h.framework/smtp",
3
- "version": "0.3.0",
3
+ "version": "0.4.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.6.0",
36
35
  "typescript": "^5.9.3",
37
36
  "vitest": "^4.0.18"
38
37
  },
39
38
  "peerDependencies": {
40
- "@t4h.framework/core": "^0.3.0"
39
+ "@t4h.framework/core": "^0.6.0"
41
40
  },
42
41
  "packageManager": "yarn@4.12.0",
43
42
  "engines": {