pipework 0.69.0 → 0.71.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.
- package/CHANGELOG.md +13 -0
- package/REFERENCE.md +74 -5
- package/dist/REFERENCE.md +124 -2
- package/dist/core/config/schema.js +2 -2
- package/dist/core/config/schema.js.map +1 -1
- package/dist/data/migrate/internal-definitions.d.ts.map +1 -1
- package/dist/data/migrate/internal-definitions.js +4 -2
- package/dist/data/migrate/internal-definitions.js.map +1 -1
- package/dist/infra/access-links/index.d.ts +1 -0
- package/dist/infra/access-links/index.d.ts.map +1 -1
- package/dist/infra/access-links/namespace.d.ts +24 -0
- package/dist/infra/access-links/namespace.d.ts.map +1 -1
- package/dist/infra/access-links/namespace.js +23 -0
- package/dist/infra/access-links/namespace.js.map +1 -1
- package/dist/infra/access-links/person-check.d.ts +45 -0
- package/dist/infra/access-links/person-check.d.ts.map +1 -0
- package/dist/infra/access-links/person-check.js +128 -0
- package/dist/infra/access-links/person-check.js.map +1 -0
- package/dist/infra/access-links/schema.d.ts +8 -0
- package/dist/infra/access-links/schema.d.ts.map +1 -1
- package/dist/infra/access-links/schema.js +7 -1
- package/dist/infra/access-links/schema.js.map +1 -1
- package/dist/infra/email/audit.d.ts +2 -2
- package/dist/infra/email/audit.d.ts.map +1 -1
- package/dist/infra/email/audit.js +3 -3
- package/dist/infra/email/audit.js.map +1 -1
- package/dist/infra/email/inbound.d.ts +199 -0
- package/dist/infra/email/inbound.d.ts.map +1 -0
- package/dist/infra/email/inbound.js +451 -0
- package/dist/infra/email/inbound.js.map +1 -0
- package/dist/infra/email/index.d.ts +1 -0
- package/dist/infra/email/index.d.ts.map +1 -1
- package/dist/infra/email/index.js +1 -0
- package/dist/infra/email/index.js.map +1 -1
- package/dist/infra/email/mail.d.ts +14 -1
- package/dist/infra/email/mail.d.ts.map +1 -1
- package/dist/infra/email/mail.js +39 -3
- package/dist/infra/email/mail.js.map +1 -1
- package/dist/infra/email/mime.d.ts +38 -0
- package/dist/infra/email/mime.d.ts.map +1 -0
- package/dist/infra/email/mime.js +474 -0
- package/dist/infra/email/mime.js.map +1 -0
- package/dist/infra/email/namespace.d.ts +8 -1
- package/dist/infra/email/namespace.d.ts.map +1 -1
- package/dist/infra/email/namespace.js +8 -1
- package/dist/infra/email/namespace.js.map +1 -1
- package/dist/infra/email/providers.d.ts +3 -1
- package/dist/infra/email/providers.d.ts.map +1 -1
- package/dist/infra/email/providers.js +8 -3
- package/dist/infra/email/providers.js.map +1 -1
- package/dist/infra/email/schema.d.ts +109 -0
- package/dist/infra/email/schema.d.ts.map +1 -1
- package/dist/infra/email/schema.js +39 -1
- package/dist/infra/email/schema.js.map +1 -1
- package/dist/infra/email/sns.d.ts +14 -0
- package/dist/infra/email/sns.d.ts.map +1 -1
- package/dist/infra/email/sns.js +37 -23
- package/dist/infra/email/sns.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { copyFile, mkdir, readdir, readFile, stat, unlink } from 'node:fs/promises';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import { Readable } from 'node:stream';
|
|
5
|
+
import { sql } from '../../data/db/sql.js';
|
|
6
|
+
import { pipeSystem } from '../../data/db/db.js';
|
|
7
|
+
import { schema } from '../../data/schema/namespace.js';
|
|
8
|
+
import { fitting } from '../../request/di/index.js';
|
|
9
|
+
import { getContext } from '../../request/context/store.js';
|
|
10
|
+
import { getBaseLogger } from '../../core/logging/index.js';
|
|
11
|
+
import { __flowStep } from '../../trace/runtime/flow-step.js';
|
|
12
|
+
import { auditEmail } from './audit.js';
|
|
13
|
+
import { parseMime } from './mime.js';
|
|
14
|
+
import { snsHandler } from './sns.js';
|
|
15
|
+
export const RECEIVE_JOB = 'pipework.email.receive';
|
|
16
|
+
const DEFAULT_RETENTION_DAYS = 30;
|
|
17
|
+
const DEFAULT_MAX_RAW_BYTES = 41_943_040;
|
|
18
|
+
const DEFAULT_SWEEP_LIMIT = 500;
|
|
19
|
+
export class RawNotFoundError extends Error {
|
|
20
|
+
constructor(detail) {
|
|
21
|
+
super(`raw message not found: ${detail}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export class RawTooLargeError extends Error {
|
|
25
|
+
maxBytes;
|
|
26
|
+
constructor(maxBytes) {
|
|
27
|
+
super(`raw message is larger than ${maxBytes} bytes`);
|
|
28
|
+
this.maxBytes = maxBytes;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function collect(stream, maxBytes) {
|
|
32
|
+
const parts = [];
|
|
33
|
+
let size = 0;
|
|
34
|
+
for await (const chunk of stream) {
|
|
35
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
36
|
+
size += buf.length;
|
|
37
|
+
if (size > maxBytes) {
|
|
38
|
+
if (stream instanceof Readable)
|
|
39
|
+
stream.destroy();
|
|
40
|
+
throw new RawTooLargeError(maxBytes);
|
|
41
|
+
}
|
|
42
|
+
parts.push(buf);
|
|
43
|
+
}
|
|
44
|
+
return Buffer.concat(parts);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The SES receipt source. SES stores each raw message in S3 (receipt rule action "S3" with an SNS topic)
|
|
48
|
+
* and notifies the topic; `inbound.webhook` verifies the notification and records it, and the receive job
|
|
49
|
+
* reads the object from `bucket`. A notification whose action is not S3 (for example the SNS action,
|
|
50
|
+
* which carries the message inline), or whose type is not `Received`, is answered 200 and ignored.
|
|
51
|
+
*/
|
|
52
|
+
export function sesReceipt(config) {
|
|
53
|
+
if (config.topicArns.length === 0)
|
|
54
|
+
throw new Error('[pipework] mail.sesReceipt needs at least one topic ARN in topicArns.');
|
|
55
|
+
return {
|
|
56
|
+
kind: 'ses',
|
|
57
|
+
ses: config,
|
|
58
|
+
async read(location, maxBytes) {
|
|
59
|
+
let stream;
|
|
60
|
+
try {
|
|
61
|
+
stream = await config.bucket.read(String(location['key']));
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (/\bNoSuchKey\b/.test(err instanceof Error ? err.message : String(err)))
|
|
65
|
+
throw new RawNotFoundError(String(location['key']));
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
return collect(stream, maxBytes);
|
|
69
|
+
},
|
|
70
|
+
async remove(location) {
|
|
71
|
+
await config.bucket.remove(String(location['key']));
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The development source: `.eml` files dropped in `dir` are received by `inbound.scan()`, which copies each
|
|
77
|
+
* to `<dir>/received/` (where the raw copy stays until `sweep`) and removes the original. Refused when the
|
|
78
|
+
* environment is `production`.
|
|
79
|
+
*/
|
|
80
|
+
export function inboundFolder(config) {
|
|
81
|
+
return {
|
|
82
|
+
kind: 'folder',
|
|
83
|
+
folder: config,
|
|
84
|
+
async read(location, maxBytes) {
|
|
85
|
+
const path = String(location['path']);
|
|
86
|
+
try {
|
|
87
|
+
if ((await stat(path)).size > maxBytes)
|
|
88
|
+
throw new RawTooLargeError(maxBytes);
|
|
89
|
+
return await readFile(path);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
if (err.code === 'ENOENT')
|
|
93
|
+
throw new RawNotFoundError(path);
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
async remove(location) {
|
|
98
|
+
try {
|
|
99
|
+
await unlink(String(location['path']));
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
if (err.code !== 'ENOENT')
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function toDate(value) {
|
|
109
|
+
if (value instanceof Date)
|
|
110
|
+
return value;
|
|
111
|
+
const text = String(value);
|
|
112
|
+
return new Date(/(Z|[+-]\d\d(:?\d\d)?)$/.test(text) ? text : `${text.replace(' ', 'T')}Z`);
|
|
113
|
+
}
|
|
114
|
+
/** A Postgres text-array literal, sent as one parameter and cast with `::text[]`. */
|
|
115
|
+
function textArray(values) {
|
|
116
|
+
return `{${values.map(v => `"${v.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`).join(',')}}`;
|
|
117
|
+
}
|
|
118
|
+
function verdictsOf(receipt) {
|
|
119
|
+
const status = (key) => {
|
|
120
|
+
const v = receipt[key];
|
|
121
|
+
return typeof v?.status === 'string' ? v.status : null;
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
spam: status('spamVerdict'), virus: status('virusVerdict'), spf: status('spfVerdict'),
|
|
125
|
+
dkim: status('dkimVerdict'), dmarc: status('dmarcVerdict'),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function addressOf(a) {
|
|
129
|
+
return a === null ? { address: '', name: '' } : { address: a.address, name: a.name };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Receiving email. `inbound.webhook` (SES) or `inbound.scan` (folder) records each message in
|
|
133
|
+
* `pipework_email_inbound` and enqueues the `pipework.email.receive` job, once per message. The job
|
|
134
|
+
* reads the raw message, parses it, assigns a conversation, resolves the tenant, stores the attachments
|
|
135
|
+
* as that tenant's files and calls `onMessage` with a `ReceivedEmail`.
|
|
136
|
+
*
|
|
137
|
+
* Needs blueprint `email: {}` (table `pipework_email_inbound`; `pipework_email_messages` gains `conversation_id`).
|
|
138
|
+
* The row is not tenant-scoped (it exists before a tenant is known) and holds no bodies: the raw message
|
|
139
|
+
* is the copy and `sweep` deletes it after `rawRetentionDays`.
|
|
140
|
+
*
|
|
141
|
+
* Threading: the ids in In-Reply-To then References are matched to an earlier received message's
|
|
142
|
+
* Message-ID, or, for an id `<X@region.amazonses.com>`, to a sent message whose `provider_message_id` is `X`
|
|
143
|
+
* (set the sent message's conversation with `mail.send({ conversationId })` or `inReplyTo`). The first match
|
|
144
|
+
* decides the conversation; with none the message starts one (`isNewConversation: true`). Ids are matched exactly.
|
|
145
|
+
* A received message is matched only when its tenant equals the tenant `resolveTenant` named for this message or
|
|
146
|
+
* either is null; a sent message's conversation is refused when it holds a received message of another tenant.
|
|
147
|
+
*
|
|
148
|
+
* Threading is not authentication: Message-IDs are not secret and From is forgeable. Before attaching a message to
|
|
149
|
+
* a conversation's participants, the app checks `from` against them and the DKIM and DMARC verdicts.
|
|
150
|
+
*
|
|
151
|
+
* The receive job is a trace step `pipework/email:receive` with an `EmailInbound:received` audit record on success.
|
|
152
|
+
* A handler error (or a failure reading the raw message) rethrows so the queue retries; attachments stored by
|
|
153
|
+
* an earlier attempt are reused. Set `maxAttempts` on the queue. A raw message over `maxRawBytes` is marked
|
|
154
|
+
* `failed` and not retried. Message bodies are never logged.
|
|
155
|
+
*/
|
|
156
|
+
export function createInbound(config) {
|
|
157
|
+
if (config.source.kind === 'folder' && config.environment === 'production') {
|
|
158
|
+
throw new Error('[pipework] mail.inboundFolder is refused in production.\n\n' +
|
|
159
|
+
' The stand-in reads .eml files from a folder.\n' +
|
|
160
|
+
' Configure a real source, e.g. mail.sesReceipt({ bucket, topicArns }).\n');
|
|
161
|
+
}
|
|
162
|
+
const retentionDays = Math.floor(config.rawRetentionDays ?? DEFAULT_RETENTION_DAYS);
|
|
163
|
+
const maxRawBytes = config.maxRawBytes ?? DEFAULT_MAX_RAW_BYTES;
|
|
164
|
+
if (!(retentionDays >= 0))
|
|
165
|
+
throw new Error('[pipework] inbound rawRetentionDays must be zero or more.');
|
|
166
|
+
if (!(maxRawBytes > 0))
|
|
167
|
+
throw new Error('[pipework] inbound maxRawBytes must be positive.');
|
|
168
|
+
async function record(source, sourceId, location, extra) {
|
|
169
|
+
const traceId = getContext()?.trace?.id ?? null;
|
|
170
|
+
return pipeSystem().transaction(async (tx) => {
|
|
171
|
+
const inserted = await tx.execute(sql `
|
|
172
|
+
INSERT INTO pipework_email_inbound
|
|
173
|
+
(source_id, source, raw_location, raw_expires_at, verdicts, recipients, from_address, trace_id)
|
|
174
|
+
VALUES (${sourceId}, ${source}, ${JSON.stringify(location)}::jsonb, now() + make_interval(days => ${retentionDays}::int),
|
|
175
|
+
${extra.verdicts === null ? null : JSON.stringify(extra.verdicts)}::jsonb,
|
|
176
|
+
${extra.recipients === null ? null : JSON.stringify(extra.recipients)}::jsonb, ${extra.from}, ${traceId}::uuid)
|
|
177
|
+
ON CONFLICT (source_id) DO NOTHING
|
|
178
|
+
RETURNING id`);
|
|
179
|
+
if (inserted.length === 0)
|
|
180
|
+
return null;
|
|
181
|
+
const inboundId = inserted[0]['id'];
|
|
182
|
+
await config.queue.enqueue(tx, { type: RECEIVE_JOB, payload: { inboundId }, continueFlow: true });
|
|
183
|
+
return inboundId;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async function ingestNotification(message) {
|
|
187
|
+
let parsed;
|
|
188
|
+
try {
|
|
189
|
+
parsed = JSON.parse(message);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return { ignored: true };
|
|
193
|
+
}
|
|
194
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
195
|
+
return { ignored: true };
|
|
196
|
+
const event = parsed;
|
|
197
|
+
if (event['notificationType'] !== 'Received')
|
|
198
|
+
return { ignored: true };
|
|
199
|
+
const mailPart = event['mail'];
|
|
200
|
+
const receipt = event['receipt'];
|
|
201
|
+
const action = receipt?.['action'];
|
|
202
|
+
if (typeof mailPart?.messageId !== 'string' || receipt === undefined)
|
|
203
|
+
return { ignored: true };
|
|
204
|
+
if (action?.type !== 'S3' || typeof action.bucketName !== 'string' || typeof action.objectKey !== 'string') {
|
|
205
|
+
getBaseLogger().info({ event: 'email receipt ignored', actionType: action?.type ?? null }, 'SES receipt ignored: its action is not S3');
|
|
206
|
+
return { ignored: true };
|
|
207
|
+
}
|
|
208
|
+
const recipientList = Array.isArray(receipt['recipients']) ? receipt['recipients'] : mailPart.destination;
|
|
209
|
+
const recipients = Array.isArray(recipientList) ? recipientList.filter((r) => typeof r === 'string') : [];
|
|
210
|
+
const id = await record('ses', mailPart.messageId, { bucket: action.bucketName, key: action.objectKey }, {
|
|
211
|
+
verdicts: verdictsOf(receipt), recipients, from: typeof mailPart.source === 'string' ? mailPart.source : null,
|
|
212
|
+
});
|
|
213
|
+
if (id === null)
|
|
214
|
+
getBaseLogger().info({ event: 'email receipt duplicate', sourceId: mailPart.messageId }, 'SES receipt already recorded');
|
|
215
|
+
return {};
|
|
216
|
+
}
|
|
217
|
+
async function thread(ids, recorded, tenantId) {
|
|
218
|
+
const db = pipeSystem();
|
|
219
|
+
if (ids.length > 0) {
|
|
220
|
+
const sesLocals = ids.flatMap(id => {
|
|
221
|
+
const m = /^(.+)@[^@]*amazonses\.com$/i.exec(id);
|
|
222
|
+
return m === null ? [] : [m[1]];
|
|
223
|
+
});
|
|
224
|
+
const received = await db.execute(sql `
|
|
225
|
+
SELECT message_id_header AS id, conversation_id FROM pipework_email_inbound
|
|
226
|
+
WHERE message_id_header = ANY(${textArray(ids)}::text[]) AND conversation_id IS NOT NULL
|
|
227
|
+
AND (${tenantId}::uuid IS NULL OR tenant_id IS NULL OR tenant_id = ${tenantId}::uuid)`);
|
|
228
|
+
const sent = sesLocals.length === 0 ? [] : await db.execute(sql `
|
|
229
|
+
SELECT provider_message_id AS id, conversation_id FROM pipework_email_messages
|
|
230
|
+
WHERE provider_message_id = ANY(${textArray(sesLocals)}::text[]) AND conversation_id IS NOT NULL`);
|
|
231
|
+
const byId = new Map();
|
|
232
|
+
for (const r of received)
|
|
233
|
+
byId.set(r['id'], r['conversation_id']);
|
|
234
|
+
const bySes = new Map();
|
|
235
|
+
for (const r of sent) {
|
|
236
|
+
const conversation = r['conversation_id'];
|
|
237
|
+
const foreign = tenantId === null ? [] : await db.execute(sql `
|
|
238
|
+
SELECT 1 FROM pipework_email_inbound
|
|
239
|
+
WHERE conversation_id = ${conversation}::uuid AND tenant_id IS NOT NULL AND tenant_id <> ${tenantId}::uuid LIMIT 1`);
|
|
240
|
+
if (foreign.length === 0)
|
|
241
|
+
bySes.set(r['id'], conversation);
|
|
242
|
+
}
|
|
243
|
+
for (const id of ids) {
|
|
244
|
+
const hit = byId.get(id) ?? bySes.get(/^(.+)@[^@]*amazonses\.com$/i.exec(id)?.[1] ?? '\u0000');
|
|
245
|
+
if (hit !== undefined)
|
|
246
|
+
return { conversationId: hit, isNew: false };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// No match: this message's own conversation, kept from an earlier attempt of the job.
|
|
250
|
+
if (recorded !== null)
|
|
251
|
+
return { conversationId: recorded, isNew: true };
|
|
252
|
+
return { conversationId: randomUUID(), isNew: true };
|
|
253
|
+
}
|
|
254
|
+
async function fail(id, error) {
|
|
255
|
+
await pipeSystem().execute(sql `UPDATE pipework_email_inbound SET status = 'failed', error = ${error} WHERE id = ${id}::uuid`);
|
|
256
|
+
await auditEmail('failed', id, { error }, 'EmailInbound');
|
|
257
|
+
getBaseLogger().error({ event: 'email receive failed', inboundId: id }, `received email not handled: ${error}`);
|
|
258
|
+
return { status: 'failed' };
|
|
259
|
+
}
|
|
260
|
+
const receive = __flowStep({ ref: 'pipework/email:receive' }, async (inboundId) => {
|
|
261
|
+
const db = pipeSystem();
|
|
262
|
+
const row = (await db.execute(sql `SELECT * FROM pipework_email_inbound WHERE id = ${inboundId}::uuid`))[0];
|
|
263
|
+
if (row === undefined)
|
|
264
|
+
throw new Error(`[pipework] Received email ${inboundId} does not exist.`);
|
|
265
|
+
const status = row['status'];
|
|
266
|
+
if (status === 'processed' || status === 'failed')
|
|
267
|
+
return { status };
|
|
268
|
+
if (config.source.kind === 'folder' && getContext()?.instance.config.environment === 'production') {
|
|
269
|
+
throw new Error('[pipework] mail.inboundFolder is refused in production.');
|
|
270
|
+
}
|
|
271
|
+
if (row['raw_deleted_at'] !== null)
|
|
272
|
+
return fail(inboundId, 'the raw message was deleted before it was handled');
|
|
273
|
+
let raw;
|
|
274
|
+
try {
|
|
275
|
+
raw = await config.source.read(row['raw_location'], maxRawBytes);
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
if (err instanceof RawTooLargeError || err instanceof RawNotFoundError)
|
|
279
|
+
return fail(inboundId, err.message);
|
|
280
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
281
|
+
await db.execute(sql `UPDATE pipework_email_inbound SET error = ${message} WHERE id = ${inboundId}::uuid`);
|
|
282
|
+
throw err;
|
|
283
|
+
}
|
|
284
|
+
const parsed = parseMime(raw);
|
|
285
|
+
const recipients = row['recipients'] ?? [...parsed.to, ...parsed.cc].map(a => a.address);
|
|
286
|
+
const verdicts = row['verdicts'] ?? null;
|
|
287
|
+
const unthreaded = {
|
|
288
|
+
id: inboundId,
|
|
289
|
+
messageId: parsed.messageId, inReplyTo: parsed.inReplyTo, references: parsed.references,
|
|
290
|
+
from: addressOf(parsed.from),
|
|
291
|
+
to: parsed.to.map(addressOf), cc: parsed.cc.map(addressOf), replyTo: parsed.replyTo.map(addressOf),
|
|
292
|
+
subject: parsed.subject, date: parsed.date, text: parsed.text, html: parsed.html,
|
|
293
|
+
attachments: parsed.attachments.map(a => ({
|
|
294
|
+
filename: a.filename, mediaType: a.mediaType, size: a.size, sha256: createHash('sha256').update(a.content).digest('hex'),
|
|
295
|
+
contentId: a.contentId, inline: a.inline, file: null,
|
|
296
|
+
})),
|
|
297
|
+
verdicts, recipients,
|
|
298
|
+
};
|
|
299
|
+
let tenantId = row['tenant_id'] ?? null;
|
|
300
|
+
if (tenantId === null && config.resolveTenant !== undefined)
|
|
301
|
+
tenantId = await config.resolveTenant(unthreaded);
|
|
302
|
+
const { conversationId, isNew } = await thread(parsed.threadIds, row['conversation_id'] ?? null, tenantId);
|
|
303
|
+
const base = { ...unthreaded, conversationId, isNewConversation: isNew, tenantId };
|
|
304
|
+
await db.execute(sql `
|
|
305
|
+
UPDATE pipework_email_inbound SET
|
|
306
|
+
message_id_header = ${parsed.messageId}, in_reply_to = ${parsed.inReplyTo}, "references" = ${JSON.stringify(parsed.references)}::jsonb,
|
|
307
|
+
from_address = ${base.from.address}, subject = ${parsed.subject}, conversation_id = ${conversationId}::uuid,
|
|
308
|
+
tenant_id = ${tenantId}::uuid, recipients = ${JSON.stringify(recipients)}::jsonb, error = NULL
|
|
309
|
+
WHERE id = ${inboundId}::uuid`);
|
|
310
|
+
let attachments = base.attachments;
|
|
311
|
+
if (config.storage !== undefined && tenantId !== null && parsed.attachments.length > 0) {
|
|
312
|
+
const storage = config.storage;
|
|
313
|
+
const stored = Array.isArray(row['attachment_files']) ? [...row['attachment_files']] : [];
|
|
314
|
+
const files = [];
|
|
315
|
+
for (let i = 0; i < parsed.attachments.length; i++) {
|
|
316
|
+
const part = parsed.attachments[i];
|
|
317
|
+
let file = null;
|
|
318
|
+
const earlier = stored[i];
|
|
319
|
+
if (typeof earlier === 'string')
|
|
320
|
+
file = await storage.getFor(tenantId, earlier);
|
|
321
|
+
if (file === null) {
|
|
322
|
+
file = await storage.putFor(tenantId, {
|
|
323
|
+
body: Readable.from([part.content]), mediaType: part.mediaType,
|
|
324
|
+
...(part.filename !== null ? { filename: part.filename } : {}),
|
|
325
|
+
expectedSha256: attachments[i].sha256,
|
|
326
|
+
});
|
|
327
|
+
stored[i] = file.id;
|
|
328
|
+
await db.execute(sql `UPDATE pipework_email_inbound SET attachment_files = ${JSON.stringify(stored)}::jsonb WHERE id = ${inboundId}::uuid`);
|
|
329
|
+
}
|
|
330
|
+
files.push(file);
|
|
331
|
+
}
|
|
332
|
+
attachments = attachments.map((a, i) => ({ ...a, file: files[i] ?? null }));
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
await config.onMessage({ ...base, tenantId, attachments });
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
339
|
+
await db.execute(sql `UPDATE pipework_email_inbound SET error = ${message} WHERE id = ${inboundId}::uuid`);
|
|
340
|
+
throw err;
|
|
341
|
+
}
|
|
342
|
+
const traceId = getContext()?.trace?.id ?? null;
|
|
343
|
+
await db.execute(sql `
|
|
344
|
+
UPDATE pipework_email_inbound SET status = 'processed', processed_at = now(), error = NULL, trace_id = COALESCE(${traceId}::uuid, trace_id)
|
|
345
|
+
WHERE id = ${inboundId}::uuid`);
|
|
346
|
+
await auditEmail('received', inboundId, {
|
|
347
|
+
conversationId, isNewConversation: isNew, tenantId, attachments: attachments.length,
|
|
348
|
+
}, 'EmailInbound');
|
|
349
|
+
return { status: 'processed' };
|
|
350
|
+
});
|
|
351
|
+
const receiveJob = fitting.use().job(RECEIVE_JOB)
|
|
352
|
+
.input(schema.check.object({ inboundId: schema.check.string() }))
|
|
353
|
+
.fit(async ({ input }) => receive(input.inboundId));
|
|
354
|
+
return {
|
|
355
|
+
webhook(webhook) {
|
|
356
|
+
const ses = config.source.ses;
|
|
357
|
+
if (config.source.kind !== 'ses' || ses === undefined) {
|
|
358
|
+
throw new Error('[pipework] inbound.webhook needs a mail.sesReceipt source; this inbound reads a folder.');
|
|
359
|
+
}
|
|
360
|
+
return snsHandler({
|
|
361
|
+
path: webhook.path, topicArns: ses.topicArns,
|
|
362
|
+
...(ses.region !== undefined ? { region: ses.region } : {}),
|
|
363
|
+
...(ses.fetchCert !== undefined ? { fetchCert: ses.fetchCert } : {}),
|
|
364
|
+
...(ses.confirm !== undefined ? { confirm: ses.confirm } : {}),
|
|
365
|
+
}, ingestNotification);
|
|
366
|
+
},
|
|
367
|
+
jobs: [receiveJob],
|
|
368
|
+
async scan() {
|
|
369
|
+
const folder = config.source.folder;
|
|
370
|
+
if (config.source.kind !== 'folder' || folder === undefined) {
|
|
371
|
+
throw new Error('[pipework] inbound.scan needs a mail.inboundFolder source; this inbound receives through SES.');
|
|
372
|
+
}
|
|
373
|
+
let names;
|
|
374
|
+
try {
|
|
375
|
+
names = (await readdir(folder.dir)).filter(n => n.toLowerCase().endsWith('.eml')).sort();
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
if (err.code === 'ENOENT')
|
|
379
|
+
return { received: 0, duplicates: 0 };
|
|
380
|
+
throw err;
|
|
381
|
+
}
|
|
382
|
+
const done = join(folder.dir, 'received');
|
|
383
|
+
let received = 0;
|
|
384
|
+
let duplicates = 0;
|
|
385
|
+
for (const name of names) {
|
|
386
|
+
const from = join(folder.dir, name);
|
|
387
|
+
if (!(await stat(from)).isFile())
|
|
388
|
+
continue;
|
|
389
|
+
const bytes = await readFile(from);
|
|
390
|
+
const sha = createHash('sha256').update(bytes).digest('hex');
|
|
391
|
+
await mkdir(done, { recursive: true });
|
|
392
|
+
const to = join(done, `${sha.slice(0, 12)}-${basename(name)}`);
|
|
393
|
+
await copyFile(from, to);
|
|
394
|
+
const id = await record('folder', `folder:${sha}`, { path: to }, { verdicts: null, recipients: null, from: null });
|
|
395
|
+
await unlink(from);
|
|
396
|
+
if (id === null)
|
|
397
|
+
duplicates++;
|
|
398
|
+
else
|
|
399
|
+
received++;
|
|
400
|
+
}
|
|
401
|
+
return { received, duplicates };
|
|
402
|
+
},
|
|
403
|
+
async sweep(options) {
|
|
404
|
+
const limit = options?.limit ?? DEFAULT_SWEEP_LIMIT;
|
|
405
|
+
const db = pipeSystem();
|
|
406
|
+
const due = await db.execute(sql `
|
|
407
|
+
SELECT id, raw_location FROM pipework_email_inbound
|
|
408
|
+
WHERE raw_expires_at <= now() AND raw_deleted_at IS NULL
|
|
409
|
+
ORDER BY raw_expires_at LIMIT ${limit}`);
|
|
410
|
+
let deleted = 0;
|
|
411
|
+
let failed = 0;
|
|
412
|
+
for (const row of due) {
|
|
413
|
+
try {
|
|
414
|
+
await config.source.remove(row['raw_location']);
|
|
415
|
+
await db.execute(sql `UPDATE pipework_email_inbound SET raw_deleted_at = now() WHERE id = ${row['id']}::uuid`);
|
|
416
|
+
deleted++;
|
|
417
|
+
}
|
|
418
|
+
catch (err) {
|
|
419
|
+
failed++;
|
|
420
|
+
getBaseLogger().error({ err, inboundId: row['id'] }, 'inbound sweep could not delete a raw message');
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return { deleted, failed };
|
|
424
|
+
},
|
|
425
|
+
async get(id) {
|
|
426
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id))
|
|
427
|
+
return null;
|
|
428
|
+
const row = (await pipeSystem().execute(sql `SELECT * FROM pipework_email_inbound WHERE id = ${id}::uuid`))[0];
|
|
429
|
+
if (row === undefined)
|
|
430
|
+
return null;
|
|
431
|
+
const date = (v) => (v === null || v === undefined ? null : toDate(v));
|
|
432
|
+
return {
|
|
433
|
+
id: row['id'],
|
|
434
|
+
source: row['source'],
|
|
435
|
+
status: row['status'],
|
|
436
|
+
error: row['error'] ?? null,
|
|
437
|
+
messageId: row['message_id_header'] ?? null,
|
|
438
|
+
conversationId: row['conversation_id'] ?? null,
|
|
439
|
+
tenantId: row['tenant_id'] ?? null,
|
|
440
|
+
subject: row['subject'] ?? null,
|
|
441
|
+
fromAddress: row['from_address'] ?? null,
|
|
442
|
+
verdicts: row['verdicts'] ?? null,
|
|
443
|
+
receivedAt: toDate(row['received_at']),
|
|
444
|
+
processedAt: date(row['processed_at']),
|
|
445
|
+
rawExpiresAt: toDate(row['raw_expires_at']),
|
|
446
|
+
rawDeletedAt: date(row['raw_deleted_at']),
|
|
447
|
+
};
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
//# sourceMappingURL=inbound.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbound.js","sourceRoot":"","sources":["../../../src/infra/email/inbound.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { createMail, normalizeAddress, SEND_JOB, type Mail, type MailConfig, type SendInput, type SendResult, type RenderedEmail, type EmailTemplates } from './mail.js';
|
|
2
2
|
export { ses, devStandIn, type MailProvider, type OutgoingMessage, type SesConfig } from './providers.js';
|
|
3
|
+
export { createInbound, sesReceipt, inboundFolder, RECEIVE_JOB, type Inbound, type InboundConfig, type InboundSource, type InboundRecord, type InboundScanResult, type InboundSweepResult, type ReceivedEmail, type ReceivedAttachment, type EmailAddress, type EmailVerdicts, type UnthreadedEmail, type SesReceiptConfig, } from './inbound.js';
|
|
3
4
|
export { snsWebhook, type SnsWebhookConfig } from './sns.js';
|
|
4
5
|
export { signV4, type SigV4Request, type AwsCredentials } from '../aws/sigv4.js';
|
|
5
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/infra/email/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,QAAQ,EAAE,KAAK,IAAI,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAC1G,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA;AAC5D,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,eAAe,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAA;AACzG,OAAO,EAAE,UAAU,EAAE,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAC5D,OAAO,EAAE,MAAM,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/infra/email/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,QAAQ,EAAE,KAAK,IAAI,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAC1G,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA;AAC5D,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,eAAe,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAA;AACzG,OAAO,EACL,aAAa,EAAE,UAAU,EAAE,aAAa,EAAE,WAAW,EAAE,KAAK,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,aAAa,EAAE,KAAK,aAAa,EAC/H,KAAK,iBAAiB,EAAE,KAAK,kBAAkB,EAAE,KAAK,aAAa,EAAE,KAAK,kBAAkB,EAAE,KAAK,YAAY,EAC/G,KAAK,aAAa,EAAE,KAAK,eAAe,EAAE,KAAK,gBAAgB,GAChE,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,UAAU,EAAE,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAC5D,OAAO,EAAE,MAAM,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAA"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { createMail, normalizeAddress, SEND_JOB } from './mail.js';
|
|
2
2
|
export { ses, devStandIn } from './providers.js';
|
|
3
|
+
export { createInbound, sesReceipt, inboundFolder, RECEIVE_JOB, } from './inbound.js';
|
|
3
4
|
export { snsWebhook } from './sns.js';
|
|
4
5
|
export { signV4 } from '../aws/sigv4.js';
|
|
5
6
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/infra/email/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,QAAQ,EACJ,MAAM,WAAW,CAAA;AAC5D,OAAO,EAAE,GAAG,EAAE,UAAU,EAA2D,MAAM,gBAAgB,CAAA;AACzG,OAAO,EAAE,UAAU,EAAyB,MAAM,UAAU,CAAA;AAC5D,OAAO,EAAE,MAAM,EAA0C,MAAM,iBAAiB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/infra/email/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,QAAQ,EACJ,MAAM,WAAW,CAAA;AAC5D,OAAO,EAAE,GAAG,EAAE,UAAU,EAA2D,MAAM,gBAAgB,CAAA;AACzG,OAAO,EACL,aAAa,EAAE,UAAU,EAAE,aAAa,EAAE,WAAW,GAGtD,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,UAAU,EAAyB,MAAM,UAAU,CAAA;AAC5D,OAAO,EAAE,MAAM,EAA0C,MAAM,iBAAiB,CAAA"}
|
|
@@ -29,6 +29,16 @@ export interface SendInput<T extends EmailTemplates, K extends keyof T & string>
|
|
|
29
29
|
readonly data: Parameters<T[K]>[0];
|
|
30
30
|
/** Idempotency key: a second send with the same key sends nothing. */
|
|
31
31
|
readonly key: string;
|
|
32
|
+
/**
|
|
33
|
+
* Puts the message in a conversation, so a reply to it threads there (the provider's message id
|
|
34
|
+
* is matched against `References`/`In-Reply-To` of received mail). Default: the conversation of `inReplyTo`.
|
|
35
|
+
*/
|
|
36
|
+
readonly conversationId?: string;
|
|
37
|
+
/**
|
|
38
|
+
* The id of a received message (`ReceivedEmail.id`) this one answers: the sent message carries
|
|
39
|
+
* `In-Reply-To: <its Message-ID>` and `References` with its references plus its Message-ID (at most 20 ids).
|
|
40
|
+
*/
|
|
41
|
+
readonly inReplyTo?: string;
|
|
32
42
|
}
|
|
33
43
|
export interface SendResult {
|
|
34
44
|
readonly messageId: string;
|
|
@@ -45,6 +55,8 @@ export interface Mail<T extends EmailTemplates> {
|
|
|
45
55
|
readonly jobs: readonly Handler<Record<string, unknown>, unknown>[];
|
|
46
56
|
}
|
|
47
57
|
export declare const SEND_JOB = "pipework.email.send";
|
|
58
|
+
/** The reply headers for a message answering a received one: In-Reply-To, and References capped to 20 ids and the provider's header length. */
|
|
59
|
+
export declare function replyHeaders(messageId: string, references: readonly string[]): Record<string, string>;
|
|
48
60
|
export declare function normalizeAddress(address: string): string;
|
|
49
61
|
/**
|
|
50
62
|
* Creates the transactional email interface.
|
|
@@ -56,13 +68,14 @@ export declare function normalizeAddress(address: string): string;
|
|
|
56
68
|
* provider call is a trace step and `Email:sent` / `Email:suppressed` / `Email:failed`
|
|
57
69
|
* audit records are written under it, in the trace of the flow that called `send`.
|
|
58
70
|
*
|
|
59
|
-
* Needs blueprint `email: {}` (tables `pipework_email_messages`, `pipework_email_suppressions`).
|
|
71
|
+
* Needs blueprint `email: {}` (tables `pipework_email_messages`, `pipework_email_suppressions`, `pipework_email_inbound`).
|
|
60
72
|
* A provider error returns the message to `queued` with `error` set and rethrows, so the
|
|
61
73
|
* queue retries (set `maxAttempts` on the queue); if the queue gives up the row keeps the error.
|
|
62
74
|
* A run claims the message with a conditional update (`status` `queued`, or `sending` for longer than
|
|
63
75
|
* `sendingStaleSeconds`), so a re-run of a job whose first run is still in flight does not call the provider.
|
|
64
76
|
* Residual window: a crash mid-send leaves the message `sending` for the stale window, after which a retry
|
|
65
77
|
* may send it once more; likewise if the provider accepted it and the process died before the row was marked `sent`. SES v2 has no idempotency token to close it.
|
|
78
|
+
* With `conversationId` / `inReplyTo` the message joins a conversation and carries reply headers (see `SendInput`).
|
|
66
79
|
* Message bodies and template data are never logged.
|
|
67
80
|
*/
|
|
68
81
|
export declare function createMail<T extends EmailTemplates>(config: MailConfig<T>): Mail<T>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mail.d.ts","sourceRoot":"","sources":["../../../src/infra/email/mail.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,wBAAwB,CAAA;AAOhD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAA;AACtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAGxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAElD,uFAAuF;AACvF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,sFAAsF;AAEtF,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,aAAa,CAAC,CAAA;AAEzE,MAAM,WAAW,UAAU,CAAC,CAAC,SAAS,cAAc;IAClD,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAA;IACrB,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;IAClC,gFAAgF;IAChF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,oGAAoG;IACpG,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;CACtC;AAED,MAAM,WAAW,SAAS,CAAC,CAAC,SAAS,cAAc,EAAE,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM;IAC7E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAClC,sEAAsE;IACtE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"mail.d.ts","sourceRoot":"","sources":["../../../src/infra/email/mail.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,wBAAwB,CAAA;AAOhD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAA;AACtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAGxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAElD,uFAAuF;AACvF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,sFAAsF;AAEtF,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,aAAa,CAAC,CAAA;AAEzE,MAAM,WAAW,UAAU,CAAC,CAAC,SAAS,cAAc;IAClD,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAA;IACrB,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;IAClC,gFAAgF;IAChF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,oGAAoG;IACpG,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;CACtC;AAED,MAAM,WAAW,SAAS,CAAC,CAAC,SAAS,cAAc,EAAE,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM;IAC7E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAClC,sEAAsE;IACtE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAChC;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,oFAAoF;IACpF,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,WAAW,IAAI,CAAC,CAAC,SAAS,cAAc;IAC5C;;;OAGG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;IACrF,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,SAAS,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,CAAA;CACpE;AAED,eAAO,MAAM,QAAQ,wBAAwB,CAAA;AAM7C,+IAA+I;AAC/I,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAKrG;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,cAAc,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CA6InF"}
|