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
package/dist/infra/email/mail.js
CHANGED
|
@@ -8,7 +8,18 @@ import { getBaseLogger } from '../../core/logging/index.js';
|
|
|
8
8
|
import { __flowStep } from '../../trace/runtime/flow-step.js';
|
|
9
9
|
import { auditEmail } from './audit.js';
|
|
10
10
|
export const SEND_JOB = 'pipework.email.send';
|
|
11
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12
|
+
const MAX_REFERENCES = 20;
|
|
13
|
+
const MAX_REFERENCES_CHARS = 900;
|
|
11
14
|
const ADDRESS_RE = /^[^\s@<>,;"]+@[^\s@<>,;"]+$/;
|
|
15
|
+
/** 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. */
|
|
16
|
+
export function replyHeaders(messageId, references) {
|
|
17
|
+
const ids = [...references, messageId].filter(id => /^[\x21-\x7e]+$/.test(id) && !/[<>]/.test(id));
|
|
18
|
+
const unique = ids.filter((id, i) => ids.lastIndexOf(id) === i).slice(-MAX_REFERENCES);
|
|
19
|
+
while (unique.length > 1 && unique.map(i => `<${i}>`).join(' ').length > MAX_REFERENCES_CHARS)
|
|
20
|
+
unique.shift();
|
|
21
|
+
return { 'In-Reply-To': `<${messageId}>`, 'References': unique.map(i => `<${i}>`).join(' ') };
|
|
22
|
+
}
|
|
12
23
|
export function normalizeAddress(address) {
|
|
13
24
|
return address.trim().toLowerCase();
|
|
14
25
|
}
|
|
@@ -22,13 +33,14 @@ export function normalizeAddress(address) {
|
|
|
22
33
|
* provider call is a trace step and `Email:sent` / `Email:suppressed` / `Email:failed`
|
|
23
34
|
* audit records are written under it, in the trace of the flow that called `send`.
|
|
24
35
|
*
|
|
25
|
-
* Needs blueprint `email: {}` (tables `pipework_email_messages`, `pipework_email_suppressions`).
|
|
36
|
+
* Needs blueprint `email: {}` (tables `pipework_email_messages`, `pipework_email_suppressions`, `pipework_email_inbound`).
|
|
26
37
|
* A provider error returns the message to `queued` with `error` set and rethrows, so the
|
|
27
38
|
* queue retries (set `maxAttempts` on the queue); if the queue gives up the row keeps the error.
|
|
28
39
|
* A run claims the message with a conditional update (`status` `queued`, or `sending` for longer than
|
|
29
40
|
* `sendingStaleSeconds`), so a re-run of a job whose first run is still in flight does not call the provider.
|
|
30
41
|
* Residual window: a crash mid-send leaves the message `sending` for the stale window, after which a retry
|
|
31
42
|
* 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.
|
|
43
|
+
* With `conversationId` / `inReplyTo` the message joins a conversation and carries reply headers (see `SendInput`).
|
|
32
44
|
* Message bodies and template data are never logged.
|
|
33
45
|
*/
|
|
34
46
|
export function createMail(config) {
|
|
@@ -59,6 +71,14 @@ export function createMail(config) {
|
|
|
59
71
|
getBaseLogger().info({ event: 'email suppressed send', messageId }, 'email skipped: address is suppressed');
|
|
60
72
|
return { status: 'suppressed' };
|
|
61
73
|
}
|
|
74
|
+
let headers;
|
|
75
|
+
const inReplyToInbound = row['in_reply_to_inbound'];
|
|
76
|
+
if (inReplyToInbound !== null) {
|
|
77
|
+
const parent = (await db.execute(sql `SELECT message_id_header, "references" FROM pipework_email_inbound WHERE id = ${inReplyToInbound}::uuid`))[0];
|
|
78
|
+
const parentId = parent?.['message_id_header'] ?? null;
|
|
79
|
+
if (parentId !== null)
|
|
80
|
+
headers = replyHeaders(parentId, parent['references'] ?? []);
|
|
81
|
+
}
|
|
62
82
|
const templateName = row['template'];
|
|
63
83
|
const template = config.templates[templateName];
|
|
64
84
|
let rendered = null;
|
|
@@ -95,6 +115,7 @@ export function createMail(config) {
|
|
|
95
115
|
subject: rendered.subject, text: rendered.text,
|
|
96
116
|
...(rendered.html !== undefined ? { html: rendered.html } : {}),
|
|
97
117
|
...(config.configurationSet !== undefined ? { configurationSet: config.configurationSet } : {}),
|
|
118
|
+
...(headers !== undefined ? { headers } : {}),
|
|
98
119
|
});
|
|
99
120
|
await db.execute(sql `
|
|
100
121
|
UPDATE pipework_email_messages
|
|
@@ -124,10 +145,25 @@ export function createMail(config) {
|
|
|
124
145
|
if (input.key.trim() === '') {
|
|
125
146
|
throw new ValidationError({ key: ['must not be empty'] }, 'mail.send: "key" must not be empty.');
|
|
126
147
|
}
|
|
148
|
+
if (input.conversationId !== undefined && !UUID_RE.test(input.conversationId)) {
|
|
149
|
+
throw new ValidationError({ conversationId: ['must be a UUID'] }, 'mail.send: "conversationId" must be a UUID.');
|
|
150
|
+
}
|
|
151
|
+
let conversationId = input.conversationId ?? null;
|
|
152
|
+
if (input.inReplyTo !== undefined) {
|
|
153
|
+
if (!UUID_RE.test(input.inReplyTo)) {
|
|
154
|
+
throw new ValidationError({ inReplyTo: ['must be a received message id'] }, 'mail.send: "inReplyTo" must be a received message id.');
|
|
155
|
+
}
|
|
156
|
+
const parent = (await db.execute(sql `SELECT conversation_id FROM pipework_email_inbound WHERE id = ${input.inReplyTo}::uuid`))[0];
|
|
157
|
+
if (parent === undefined) {
|
|
158
|
+
throw new ValidationError({ inReplyTo: ['no received message has this id'] }, `mail.send: no received message ${input.inReplyTo}.`);
|
|
159
|
+
}
|
|
160
|
+
conversationId ??= parent['conversation_id'] ?? null;
|
|
161
|
+
}
|
|
127
162
|
const traceId = getContext()?.trace?.id ?? null;
|
|
128
163
|
const inserted = await db.execute(sql `
|
|
129
|
-
INSERT INTO pipework_email_messages (key, to_address, template, data, trace_id)
|
|
130
|
-
VALUES (${input.key}, ${input.to.trim()}, ${input.template}, ${JSON.stringify(input.data ?? null)}::jsonb, ${traceId}::uuid
|
|
164
|
+
INSERT INTO pipework_email_messages (key, to_address, template, data, trace_id, conversation_id, in_reply_to_inbound)
|
|
165
|
+
VALUES (${input.key}, ${input.to.trim()}, ${input.template}, ${JSON.stringify(input.data ?? null)}::jsonb, ${traceId}::uuid,
|
|
166
|
+
${conversationId}::uuid, ${input.inReplyTo ?? null}::uuid)
|
|
131
167
|
ON CONFLICT (key) DO NOTHING
|
|
132
168
|
RETURNING id`);
|
|
133
169
|
if (inserted.length === 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mail.js","sourceRoot":"","sources":["../../../src/infra/email/mail.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAA;AAE1C,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,gCAAgC,CAAA;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAA;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAG3D,OAAO,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAA;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"mail.js","sourceRoot":"","sources":["../../../src/infra/email/mail.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface ParsedAddress {
|
|
2
|
+
readonly address: string;
|
|
3
|
+
readonly name: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ParsedAttachment {
|
|
6
|
+
readonly filename: string | null;
|
|
7
|
+
readonly mediaType: string;
|
|
8
|
+
readonly size: number;
|
|
9
|
+
readonly contentId: string | null;
|
|
10
|
+
readonly inline: boolean;
|
|
11
|
+
readonly content: Buffer;
|
|
12
|
+
}
|
|
13
|
+
export interface ParsedMessage {
|
|
14
|
+
/** Message-ID without angle brackets, or null. */
|
|
15
|
+
readonly messageId: string | null;
|
|
16
|
+
/** The first In-Reply-To id without angle brackets, or null. */
|
|
17
|
+
readonly inReplyTo: string | null;
|
|
18
|
+
/** In-Reply-To ids (first) then References ids, without angle brackets, duplicates removed. */
|
|
19
|
+
readonly threadIds: readonly string[];
|
|
20
|
+
/** References ids without angle brackets, in order. */
|
|
21
|
+
readonly references: readonly string[];
|
|
22
|
+
readonly from: ParsedAddress | null;
|
|
23
|
+
readonly to: readonly ParsedAddress[];
|
|
24
|
+
readonly cc: readonly ParsedAddress[];
|
|
25
|
+
readonly replyTo: readonly ParsedAddress[];
|
|
26
|
+
readonly subject: string;
|
|
27
|
+
readonly date: Date | null;
|
|
28
|
+
readonly text: string | null;
|
|
29
|
+
readonly html: string | null;
|
|
30
|
+
readonly attachments: readonly ParsedAttachment[];
|
|
31
|
+
}
|
|
32
|
+
/** Decodes RFC 2047 encoded words in `text`. Adjacent words are joined without the whitespace between them, and words in the same charset are decoded together so a multibyte character split across words survives. */
|
|
33
|
+
export declare function decodeWords(text: string): string;
|
|
34
|
+
/** Parses an address list (RFC 5322 mailboxes, groups, quoted names, comments, encoded words). */
|
|
35
|
+
export declare function parseAddresses(text: string | undefined): ParsedAddress[];
|
|
36
|
+
/** Parses a raw RFC 5322 message. Never throws; unreadable input yields as much as could be read. */
|
|
37
|
+
export declare function parseMime(raw: Buffer): ParsedMessage;
|
|
38
|
+
//# sourceMappingURL=mime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mime.d.ts","sourceRoot":"","sources":["../../../src/infra/email/mime.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,kDAAkD;IAClD,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,gEAAgE;IAChE,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,+FAA+F;IAC/F,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IACrC,uDAAuD;IACvD,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;IACtC,QAAQ,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAAA;IACnC,QAAQ,CAAC,EAAE,EAAE,SAAS,aAAa,EAAE,CAAA;IACrC,QAAQ,CAAC,EAAE,EAAE,SAAS,aAAa,EAAE,CAAA;IACrC,QAAQ,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,CAAA;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,QAAQ,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,CAAA;CAClD;AAyED,wNAAwN;AACxN,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA2BhD;AA6ED,kGAAkG;AAClG,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa,EAAE,CAsCxE;AAiJD,qGAAqG;AACrG,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CA4BpD"}
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
// A hand-written MIME parser for received mail. Pure: bytes in, a plain object out.
|
|
2
|
+
// It never throws; malformed input degrades to the best reading. Internal to the
|
|
3
|
+
// inbound pipeline (not public API); exported for its tests.
|
|
4
|
+
//
|
|
5
|
+
// The message is held as a latin1 string (one char per byte) while it is split, so
|
|
6
|
+
// offsets are byte offsets and slices convert back losslessly; charsets are decoded
|
|
7
|
+
// only on the final text of a header or body.
|
|
8
|
+
const MAX_DEPTH = 20;
|
|
9
|
+
function header(entity, name) {
|
|
10
|
+
return entity.headers.find(h => h.name === name)?.value;
|
|
11
|
+
}
|
|
12
|
+
function splitEntity(raw) {
|
|
13
|
+
let at = -1;
|
|
14
|
+
let bodyAt = 0;
|
|
15
|
+
if (raw.startsWith('\r\n')) {
|
|
16
|
+
at = 0;
|
|
17
|
+
bodyAt = 2;
|
|
18
|
+
}
|
|
19
|
+
else if (raw.startsWith('\n')) {
|
|
20
|
+
at = 0;
|
|
21
|
+
bodyAt = 1;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
const m = /\r?\n\r?\n/.exec(raw);
|
|
25
|
+
if (m !== null) {
|
|
26
|
+
at = m.index;
|
|
27
|
+
bodyAt = m.index + m[0].length;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const head = at < 0 ? raw : raw.slice(0, at);
|
|
31
|
+
const body = at < 0 ? '' : raw.slice(bodyAt);
|
|
32
|
+
const headers = [];
|
|
33
|
+
let current = null;
|
|
34
|
+
for (const line of head.split(/\r?\n/)) {
|
|
35
|
+
if (/^[ \t]/.test(line)) {
|
|
36
|
+
if (current !== null)
|
|
37
|
+
current.value += ` ${line.trim()}`;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const colon = line.indexOf(':');
|
|
41
|
+
if (colon <= 0 || /[\s\x00-\x1f]/.test(line.slice(0, colon))) {
|
|
42
|
+
current = null;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
current = { name: line.slice(0, colon).toLowerCase(), value: line.slice(colon + 1).trim() };
|
|
46
|
+
headers.push(current);
|
|
47
|
+
}
|
|
48
|
+
// A header with 8-bit bytes is UTF-8 when it is valid UTF-8, otherwise latin1 as read.
|
|
49
|
+
return { headers: headers.map(h => ({ name: h.name, value: decodeRawHeader(h.value) })), body };
|
|
50
|
+
}
|
|
51
|
+
function decodeRawHeader(value) {
|
|
52
|
+
if (!/[^\x00-\x7f]/.test(value))
|
|
53
|
+
return value;
|
|
54
|
+
try {
|
|
55
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(Buffer.from(value, 'latin1'));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function decodeCharset(bytes, charset) {
|
|
62
|
+
const label = (charset ?? 'utf-8').trim().replace(/^"|"$/g, '').toLowerCase();
|
|
63
|
+
try {
|
|
64
|
+
return new TextDecoder(label === '' ? 'utf-8' : label).decode(bytes);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return new TextDecoder('utf-8').decode(bytes);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// ---- RFC 2047 encoded words ----
|
|
71
|
+
function decodeQ(text) {
|
|
72
|
+
const bytes = [];
|
|
73
|
+
for (let i = 0; i < text.length; i++) {
|
|
74
|
+
const c = text[i];
|
|
75
|
+
if (c === '_')
|
|
76
|
+
bytes.push(0x20);
|
|
77
|
+
else if (c === '=' && /^[0-9a-fA-F]{2}$/.test(text.slice(i + 1, i + 3))) {
|
|
78
|
+
bytes.push(parseInt(text.slice(i + 1, i + 3), 16));
|
|
79
|
+
i += 2;
|
|
80
|
+
}
|
|
81
|
+
else
|
|
82
|
+
bytes.push(c.charCodeAt(0) & 0xff);
|
|
83
|
+
}
|
|
84
|
+
return Buffer.from(bytes);
|
|
85
|
+
}
|
|
86
|
+
const ENCODED_WORD = /=\?([^?\s]+)\?([bBqQ])\?([^?\s]*)\?=/g;
|
|
87
|
+
/** Decodes RFC 2047 encoded words in `text`. Adjacent words are joined without the whitespace between them, and words in the same charset are decoded together so a multibyte character split across words survives. */
|
|
88
|
+
export function decodeWords(text) {
|
|
89
|
+
if (!text.includes('=?'))
|
|
90
|
+
return text;
|
|
91
|
+
const out = [];
|
|
92
|
+
let run = null;
|
|
93
|
+
const flush = () => {
|
|
94
|
+
if (run !== null)
|
|
95
|
+
out.push(decodeCharset(Buffer.concat(run.bytes), run.charset));
|
|
96
|
+
run = null;
|
|
97
|
+
};
|
|
98
|
+
let last = 0;
|
|
99
|
+
for (const m of text.matchAll(ENCODED_WORD)) {
|
|
100
|
+
const between = text.slice(last, m.index);
|
|
101
|
+
if (run !== null && between.trim() === '') {
|
|
102
|
+
// whitespace between adjacent encoded words is dropped
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
flush();
|
|
106
|
+
out.push(between);
|
|
107
|
+
}
|
|
108
|
+
const charset = m[1].split('*')[0].toLowerCase();
|
|
109
|
+
const bytes = m[2].toLowerCase() === 'b' ? Buffer.from(m[3], 'base64') : decodeQ(m[3]);
|
|
110
|
+
if (run !== null && run.charset !== charset)
|
|
111
|
+
flush();
|
|
112
|
+
if (run === null)
|
|
113
|
+
run = { charset, bytes: [] };
|
|
114
|
+
run.bytes.push(bytes);
|
|
115
|
+
last = m.index + m[0].length;
|
|
116
|
+
}
|
|
117
|
+
flush();
|
|
118
|
+
out.push(text.slice(last));
|
|
119
|
+
return out.join('');
|
|
120
|
+
}
|
|
121
|
+
// ---- structured header values ----
|
|
122
|
+
function unquote(text) {
|
|
123
|
+
const t = text.trim();
|
|
124
|
+
if (t.length >= 2 && t.startsWith('"') && t.endsWith('"'))
|
|
125
|
+
return t.slice(1, -1).replace(/\\(.)/g, '$1');
|
|
126
|
+
return t;
|
|
127
|
+
}
|
|
128
|
+
function splitTop(text, separator) {
|
|
129
|
+
const parts = [];
|
|
130
|
+
let cur = '';
|
|
131
|
+
let quoted = false;
|
|
132
|
+
for (let i = 0; i < text.length; i++) {
|
|
133
|
+
const c = text[i];
|
|
134
|
+
if (quoted) {
|
|
135
|
+
cur += c;
|
|
136
|
+
if (c === '\\' && i + 1 < text.length)
|
|
137
|
+
cur += text[++i];
|
|
138
|
+
else if (c === '"')
|
|
139
|
+
quoted = false;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (c === '"') {
|
|
143
|
+
quoted = true;
|
|
144
|
+
cur += c;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (c === separator) {
|
|
148
|
+
parts.push(cur);
|
|
149
|
+
cur = '';
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
cur += c;
|
|
153
|
+
}
|
|
154
|
+
parts.push(cur);
|
|
155
|
+
return parts;
|
|
156
|
+
}
|
|
157
|
+
function percentBytes(text) {
|
|
158
|
+
const bytes = [];
|
|
159
|
+
for (let i = 0; i < text.length; i++) {
|
|
160
|
+
if (text[i] === '%' && /^[0-9a-fA-F]{2}$/.test(text.slice(i + 1, i + 3))) {
|
|
161
|
+
bytes.push(parseInt(text.slice(i + 1, i + 3), 16));
|
|
162
|
+
i += 2;
|
|
163
|
+
}
|
|
164
|
+
else
|
|
165
|
+
bytes.push(text.charCodeAt(i) & 0xff);
|
|
166
|
+
}
|
|
167
|
+
return Buffer.from(bytes);
|
|
168
|
+
}
|
|
169
|
+
/** Parses `value; name=param; name*=utf-8''enc; name*0*=...` and resolves RFC 2231 extended and continued parameters. */
|
|
170
|
+
function parseStructured(text) {
|
|
171
|
+
if (text === undefined)
|
|
172
|
+
return { value: '', params: new Map() };
|
|
173
|
+
const [first, ...rest] = splitTop(text, ';');
|
|
174
|
+
const plain = new Map();
|
|
175
|
+
const groups = new Map();
|
|
176
|
+
for (const part of rest) {
|
|
177
|
+
const eq = part.indexOf('=');
|
|
178
|
+
if (eq <= 0)
|
|
179
|
+
continue;
|
|
180
|
+
const key = part.slice(0, eq).trim().toLowerCase();
|
|
181
|
+
const value = unquote(part.slice(eq + 1));
|
|
182
|
+
const m = /^([^*]+)(?:\*(\d+))?(\*)?$/.exec(key);
|
|
183
|
+
if (m === null || (m[2] === undefined && m[3] === undefined)) {
|
|
184
|
+
plain.set(key, value);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const list = groups.get(m[1]) ?? [];
|
|
188
|
+
list.push({ n: m[2] === undefined ? 0 : Number(m[2]), ext: m[3] !== undefined, value });
|
|
189
|
+
groups.set(m[1], list);
|
|
190
|
+
}
|
|
191
|
+
const params = new Map(plain);
|
|
192
|
+
for (const [name, list] of groups) {
|
|
193
|
+
list.sort((a, b) => a.n - b.n);
|
|
194
|
+
let charset;
|
|
195
|
+
const chunks = [];
|
|
196
|
+
list.forEach((seg, i) => {
|
|
197
|
+
let v = seg.value;
|
|
198
|
+
if (i === 0 && seg.ext) {
|
|
199
|
+
const m = /^([^']*)'[^']*'(.*)$/s.exec(v);
|
|
200
|
+
if (m !== null) {
|
|
201
|
+
charset = m[1] === '' ? undefined : m[1];
|
|
202
|
+
v = m[2];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
chunks.push(seg.ext ? percentBytes(v) : Buffer.from(v, 'latin1'));
|
|
206
|
+
});
|
|
207
|
+
params.set(name, decodeCharset(Buffer.concat(chunks), charset));
|
|
208
|
+
}
|
|
209
|
+
return { value: (first ?? '').trim(), params };
|
|
210
|
+
}
|
|
211
|
+
// ---- addresses ----
|
|
212
|
+
/** Parses an address list (RFC 5322 mailboxes, groups, quoted names, comments, encoded words). */
|
|
213
|
+
export function parseAddresses(text) {
|
|
214
|
+
if (text === undefined || text.trim() === '')
|
|
215
|
+
return [];
|
|
216
|
+
const mailboxes = [];
|
|
217
|
+
let cur = '';
|
|
218
|
+
let quoted = false;
|
|
219
|
+
let angle = false;
|
|
220
|
+
let comment = 0;
|
|
221
|
+
for (let i = 0; i < text.length; i++) {
|
|
222
|
+
const c = text[i];
|
|
223
|
+
if (quoted) {
|
|
224
|
+
cur += c;
|
|
225
|
+
if (c === '\\' && i + 1 < text.length)
|
|
226
|
+
cur += text[++i];
|
|
227
|
+
else if (c === '"')
|
|
228
|
+
quoted = false;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (comment > 0) {
|
|
232
|
+
cur += c;
|
|
233
|
+
if (c === '\\' && i + 1 < text.length)
|
|
234
|
+
cur += text[++i];
|
|
235
|
+
else if (c === '(')
|
|
236
|
+
comment++;
|
|
237
|
+
else if (c === ')')
|
|
238
|
+
comment--;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (c === '"') {
|
|
242
|
+
quoted = true;
|
|
243
|
+
cur += c;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (c === '(') {
|
|
247
|
+
comment = 1;
|
|
248
|
+
cur += c;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (c === '<')
|
|
252
|
+
angle = true;
|
|
253
|
+
else if (c === '>')
|
|
254
|
+
angle = false;
|
|
255
|
+
if (!angle && c === ':') {
|
|
256
|
+
cur = '';
|
|
257
|
+
continue;
|
|
258
|
+
} // group name
|
|
259
|
+
if (!angle && (c === ',' || c === ';')) {
|
|
260
|
+
mailboxes.push(cur);
|
|
261
|
+
cur = '';
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
cur += c;
|
|
265
|
+
}
|
|
266
|
+
mailboxes.push(cur);
|
|
267
|
+
const result = [];
|
|
268
|
+
for (const box of mailboxes) {
|
|
269
|
+
const parsed = parseMailbox(box);
|
|
270
|
+
if (parsed !== null)
|
|
271
|
+
result.push(parsed);
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
function stripComments(text) {
|
|
276
|
+
const comments = [];
|
|
277
|
+
let out = '';
|
|
278
|
+
let depth = 0;
|
|
279
|
+
let quoted = false;
|
|
280
|
+
let current = '';
|
|
281
|
+
for (let i = 0; i < text.length; i++) {
|
|
282
|
+
const c = text[i];
|
|
283
|
+
if (quoted) {
|
|
284
|
+
out += c;
|
|
285
|
+
if (c === '\\' && i + 1 < text.length)
|
|
286
|
+
out += text[++i];
|
|
287
|
+
else if (c === '"')
|
|
288
|
+
quoted = false;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (depth > 0) {
|
|
292
|
+
if (c === '\\' && i + 1 < text.length) {
|
|
293
|
+
current += text[++i];
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (c === '(')
|
|
297
|
+
depth++;
|
|
298
|
+
else if (c === ')') {
|
|
299
|
+
depth--;
|
|
300
|
+
if (depth === 0) {
|
|
301
|
+
comments.push(current);
|
|
302
|
+
current = '';
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
current += c;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (c === '"') {
|
|
310
|
+
quoted = true;
|
|
311
|
+
out += c;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (c === '(') {
|
|
315
|
+
depth = 1;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
out += c;
|
|
319
|
+
}
|
|
320
|
+
return { text: out, comments };
|
|
321
|
+
}
|
|
322
|
+
function parseMailbox(box) {
|
|
323
|
+
const { text, comments } = stripComments(box);
|
|
324
|
+
const t = text.trim();
|
|
325
|
+
if (t === '')
|
|
326
|
+
return null;
|
|
327
|
+
const lt = t.lastIndexOf('<');
|
|
328
|
+
const gt = t.lastIndexOf('>');
|
|
329
|
+
if (lt >= 0 && gt > lt) {
|
|
330
|
+
const address = t.slice(lt + 1, gt).trim().replace(/^.*:/, '');
|
|
331
|
+
const name = decodeWords(unquote(t.slice(0, lt)));
|
|
332
|
+
return address === '' ? null : { address, name };
|
|
333
|
+
}
|
|
334
|
+
const address = unquote(t);
|
|
335
|
+
if (!address.includes('@'))
|
|
336
|
+
return null;
|
|
337
|
+
return { address, name: comments.length > 0 ? decodeWords(comments[0].trim()) : '' };
|
|
338
|
+
}
|
|
339
|
+
function idsOf(text) {
|
|
340
|
+
if (text === undefined)
|
|
341
|
+
return [];
|
|
342
|
+
return [...text.matchAll(/<([^<>\s]+)>/g)].map(m => m[1]);
|
|
343
|
+
}
|
|
344
|
+
// ---- bodies ----
|
|
345
|
+
function decodeTransfer(body, encoding) {
|
|
346
|
+
const e = (encoding ?? '7bit').trim().toLowerCase();
|
|
347
|
+
if (e === 'base64')
|
|
348
|
+
return Buffer.from(body.replace(/[^A-Za-z0-9+/=_-]/g, ''), 'base64');
|
|
349
|
+
if (e === 'quoted-printable') {
|
|
350
|
+
const joined = body.replace(/[ \t]+(?=\r?\n)/g, '').replace(/=\r?\n/g, '');
|
|
351
|
+
const bytes = [];
|
|
352
|
+
for (let i = 0; i < joined.length; i++) {
|
|
353
|
+
const c = joined[i];
|
|
354
|
+
if (c === '=' && /^[0-9a-fA-F]{2}$/.test(joined.slice(i + 1, i + 3))) {
|
|
355
|
+
bytes.push(parseInt(joined.slice(i + 1, i + 3), 16));
|
|
356
|
+
i += 2;
|
|
357
|
+
}
|
|
358
|
+
else
|
|
359
|
+
bytes.push(joined.charCodeAt(i) & 0xff);
|
|
360
|
+
}
|
|
361
|
+
return Buffer.from(bytes);
|
|
362
|
+
}
|
|
363
|
+
return Buffer.from(body, 'latin1');
|
|
364
|
+
}
|
|
365
|
+
function splitMultipart(body, boundary) {
|
|
366
|
+
const delim = `--${boundary}`;
|
|
367
|
+
const parts = [];
|
|
368
|
+
let start = -1;
|
|
369
|
+
let pos = 0;
|
|
370
|
+
for (;;) {
|
|
371
|
+
const idx = body.indexOf(delim, pos);
|
|
372
|
+
if (idx < 0)
|
|
373
|
+
break;
|
|
374
|
+
pos = idx + 1;
|
|
375
|
+
if (idx > 0 && body[idx - 1] !== '\n')
|
|
376
|
+
continue;
|
|
377
|
+
const after = idx + delim.length;
|
|
378
|
+
const eol = body.indexOf('\n', after);
|
|
379
|
+
const rest = body.slice(after, eol < 0 ? body.length : eol);
|
|
380
|
+
const closing = rest.startsWith('--');
|
|
381
|
+
if (!closing && !/^[ \t]*\r?$/.test(rest))
|
|
382
|
+
continue;
|
|
383
|
+
if (start >= 0) {
|
|
384
|
+
let end = idx;
|
|
385
|
+
if (body[end - 1] === '\n')
|
|
386
|
+
end--;
|
|
387
|
+
if (body[end - 1] === '\r')
|
|
388
|
+
end--;
|
|
389
|
+
parts.push(body.slice(start, Math.max(start, end)));
|
|
390
|
+
start = -1;
|
|
391
|
+
}
|
|
392
|
+
if (closing || eol < 0)
|
|
393
|
+
return parts;
|
|
394
|
+
start = eol + 1;
|
|
395
|
+
pos = start;
|
|
396
|
+
}
|
|
397
|
+
if (start >= 0)
|
|
398
|
+
parts.push(body.slice(start)); // no closing boundary
|
|
399
|
+
return parts;
|
|
400
|
+
}
|
|
401
|
+
function walk(entity, depth, out) {
|
|
402
|
+
const type = parseStructured(header(entity, 'content-type'));
|
|
403
|
+
const mediaType = (type.value === '' ? 'text/plain' : type.value).toLowerCase();
|
|
404
|
+
const disposition = parseStructured(header(entity, 'content-disposition'));
|
|
405
|
+
const disp = disposition.value.toLowerCase();
|
|
406
|
+
const contentId = idsOf(header(entity, 'content-id'))[0] ?? null;
|
|
407
|
+
if (mediaType.startsWith('multipart/') && depth < MAX_DEPTH) {
|
|
408
|
+
const boundary = type.params.get('boundary');
|
|
409
|
+
if (boundary !== undefined && boundary !== '') {
|
|
410
|
+
for (const part of splitMultipart(entity.body, boundary))
|
|
411
|
+
walk(splitEntity(part), depth + 1, out);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const rawName = disposition.params.get('filename') ?? type.params.get('name');
|
|
416
|
+
const filename = rawName === undefined ? null : decodeWords(rawName);
|
|
417
|
+
const isText = mediaType === 'text/plain' || mediaType === 'text/html';
|
|
418
|
+
const attached = disp === 'attachment' || filename !== null || !isText || mediaType.startsWith('multipart/');
|
|
419
|
+
const content = decodeTransfer(entity.body, header(entity, 'content-transfer-encoding'));
|
|
420
|
+
if (!attached) {
|
|
421
|
+
const decoded = decodeCharset(content, type.params.get('charset'));
|
|
422
|
+
if (mediaType === 'text/html') {
|
|
423
|
+
if (out.html === null)
|
|
424
|
+
out.html = decoded;
|
|
425
|
+
}
|
|
426
|
+
else if (out.text === null)
|
|
427
|
+
out.text = decoded;
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
out.attachments.push({
|
|
431
|
+
filename,
|
|
432
|
+
mediaType: mediaType.startsWith('multipart/') ? 'application/octet-stream' : mediaType,
|
|
433
|
+
size: content.length,
|
|
434
|
+
contentId,
|
|
435
|
+
inline: disp === 'inline' || (disp === '' && contentId !== null),
|
|
436
|
+
content,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
const EMPTY = {
|
|
440
|
+
messageId: null, inReplyTo: null, threadIds: [], references: [], from: null, to: [], cc: [], replyTo: [],
|
|
441
|
+
subject: '', date: null, text: null, html: null, attachments: [],
|
|
442
|
+
};
|
|
443
|
+
/** Parses a raw RFC 5322 message. Never throws; unreadable input yields as much as could be read. */
|
|
444
|
+
export function parseMime(raw) {
|
|
445
|
+
try {
|
|
446
|
+
const entity = splitEntity(raw.toString('latin1'));
|
|
447
|
+
const out = { text: null, html: null, attachments: [] };
|
|
448
|
+
walk(entity, 0, out);
|
|
449
|
+
const inReplyToIds = idsOf(header(entity, 'in-reply-to'));
|
|
450
|
+
const references = idsOf(header(entity, 'references'));
|
|
451
|
+
const threadIds = [...new Set([...inReplyToIds, ...references])];
|
|
452
|
+
const date = header(entity, 'date');
|
|
453
|
+
const parsedDate = date === undefined ? null : new Date(date);
|
|
454
|
+
return {
|
|
455
|
+
messageId: idsOf(header(entity, 'message-id'))[0] ?? null,
|
|
456
|
+
inReplyTo: inReplyToIds[0] ?? null,
|
|
457
|
+
threadIds,
|
|
458
|
+
references,
|
|
459
|
+
from: parseAddresses(header(entity, 'from'))[0] ?? null,
|
|
460
|
+
to: parseAddresses(header(entity, 'to')),
|
|
461
|
+
cc: parseAddresses(header(entity, 'cc')),
|
|
462
|
+
replyTo: parseAddresses(header(entity, 'reply-to')),
|
|
463
|
+
subject: decodeWords(header(entity, 'subject') ?? ''),
|
|
464
|
+
date: parsedDate === null || Number.isNaN(parsedDate.getTime()) ? null : parsedDate,
|
|
465
|
+
text: out.text,
|
|
466
|
+
html: out.html,
|
|
467
|
+
attachments: out.attachments,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
return EMPTY;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
//# sourceMappingURL=mime.js.map
|