@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,3 @@
1
+ import { SmtpSendEmailClaim } from '../models/SmtpSendEmailClaim.js';
2
+ export declare function mockSmtpClaim(implementation?: Partial<SmtpSendEmailClaim>): import("@t4h.framework/core").ClaimProvider<SmtpSendEmailClaim>;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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.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.3.0"
39
+ "@t4h.framework/core": "^0.8.0"
41
40
  },
42
41
  "packageManager": "yarn@4.12.0",
43
42
  "engines": {