@dawnlit/outlook-bridge 1.1.0 → 2.0.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/README.md +150 -183
- package/dist/backend.d.ts +151 -0
- package/dist/backend.js +3 -0
- package/dist/backend.js.map +1 -0
- package/dist/bridge.d.ts +56 -0
- package/dist/bridge.js +507 -0
- package/dist/bridge.js.map +1 -0
- package/dist/errors.d.ts +81 -43
- package/dist/errors.js +98 -52
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +10 -10
- package/dist/index.js +25 -52
- package/dist/index.js.map +1 -1
- package/dist/mac/accounts.d.ts +7 -2
- package/dist/mac/accounts.js +23 -6
- package/dist/mac/accounts.js.map +1 -1
- package/dist/mac/attachments.d.ts +5 -25
- package/dist/mac/attachments.js +40 -99
- package/dist/mac/attachments.js.map +1 -1
- package/dist/mac/bounces.d.ts +12 -27
- package/dist/mac/bounces.js +95 -112
- package/dist/mac/bounces.js.map +1 -1
- package/dist/mac/cleanup.d.ts +11 -25
- package/dist/mac/cleanup.js +50 -76
- package/dist/mac/cleanup.js.map +1 -1
- package/dist/mac/drafts.d.ts +13 -34
- package/dist/mac/drafts.js +95 -169
- package/dist/mac/drafts.js.map +1 -1
- package/dist/mac/folders.d.ts +10 -21
- package/dist/mac/folders.js +33 -47
- package/dist/mac/folders.js.map +1 -1
- package/dist/mac/index.d.ts +2 -22
- package/dist/mac/index.js +19 -81
- package/dist/mac/index.js.map +1 -1
- package/dist/mac/profile.d.ts +13 -0
- package/dist/mac/profile.js +149 -0
- package/dist/mac/profile.js.map +1 -0
- package/dist/mac/read.d.ts +18 -36
- package/dist/mac/read.js +154 -208
- package/dist/mac/read.js.map +1 -1
- package/dist/mac/run.d.ts +35 -39
- package/dist/mac/run.js +97 -102
- package/dist/mac/run.js.map +1 -1
- package/dist/mac/scripts.d.ts +78 -86
- package/dist/mac/scripts.js +125 -115
- package/dist/mac/scripts.js.map +1 -1
- package/dist/mac/send.d.ts +10 -20
- package/dist/mac/send.js +64 -86
- package/dist/mac/send.js.map +1 -1
- package/dist/mac/signatures.d.ts +2 -2
- package/dist/mac/signatures.js +9 -11
- package/dist/mac/signatures.js.map +1 -1
- package/dist/mac/templates.d.ts +14 -26
- package/dist/mac/templates.js +75 -113
- package/dist/mac/templates.js.map +1 -1
- package/dist/mail.d.ts +77 -45
- package/dist/mail.js +166 -73
- package/dist/mail.js.map +1 -1
- package/dist/runtime.d.ts +76 -43
- package/dist/runtime.js +103 -48
- package/dist/runtime.js.map +1 -1
- package/dist/shared/args.d.ts +48 -0
- package/dist/shared/args.js +216 -0
- package/dist/shared/args.js.map +1 -0
- package/dist/shared/attachmentMatch.d.ts +28 -16
- package/dist/shared/attachmentMatch.js +60 -22
- package/dist/shared/attachmentMatch.js.map +1 -1
- package/dist/shared/bounceReport.d.ts +11 -0
- package/dist/shared/bounceReport.js +45 -0
- package/dist/shared/bounceReport.js.map +1 -0
- package/dist/shared/bounceRules.d.ts +38 -10
- package/dist/shared/bounceRules.js +50 -32
- package/dist/shared/bounceRules.js.map +1 -1
- package/dist/shared/exec.d.ts +21 -0
- package/dist/shared/exec.js +47 -0
- package/dist/shared/exec.js.map +1 -0
- package/dist/shared/json.d.ts +12 -6
- package/dist/shared/json.js +29 -23
- package/dist/shared/json.js.map +1 -1
- package/dist/shared/locate.d.ts +39 -0
- package/dist/shared/locate.js +129 -0
- package/dist/shared/locate.js.map +1 -0
- package/dist/shared/replyBody.d.ts +42 -32
- package/dist/shared/replyBody.js +84 -71
- package/dist/shared/replyBody.js.map +1 -1
- package/dist/templateBody.d.ts +63 -0
- package/dist/{outlookTemplateSections.js → templateBody.js} +72 -72
- package/dist/templateBody.js.map +1 -0
- package/dist/types.d.ts +360 -165
- package/dist/types.js +8 -9
- package/dist/types.js.map +1 -1
- package/dist/windows/accounts.d.ts +10 -1
- package/dist/windows/accounts.js +25 -13
- package/dist/windows/accounts.js.map +1 -1
- package/dist/windows/attachments.d.ts +9 -24
- package/dist/windows/attachments.js +94 -119
- package/dist/windows/attachments.js.map +1 -1
- package/dist/windows/bounces.d.ts +13 -26
- package/dist/windows/bounces.js +169 -200
- package/dist/windows/bounces.js.map +1 -1
- package/dist/windows/cleanup.d.ts +19 -23
- package/dist/windows/cleanup.js +93 -111
- package/dist/windows/cleanup.js.map +1 -1
- package/dist/windows/drafts.d.ts +16 -35
- package/dist/windows/drafts.js +167 -253
- package/dist/windows/drafts.js.map +1 -1
- package/dist/windows/folders.d.ts +8 -13
- package/dist/windows/folders.js +45 -52
- package/dist/windows/folders.js.map +1 -1
- package/dist/windows/index.d.ts +2 -27
- package/dist/windows/index.js +11 -65
- package/dist/windows/index.js.map +1 -1
- package/dist/windows/read.d.ts +24 -45
- package/dist/windows/read.js +266 -279
- package/dist/windows/read.js.map +1 -1
- package/dist/windows/run.d.ts +39 -39
- package/dist/windows/run.js +151 -111
- package/dist/windows/run.js.map +1 -1
- package/dist/windows/scripts.d.ts +71 -38
- package/dist/windows/scripts.js +123 -71
- package/dist/windows/scripts.js.map +1 -1
- package/dist/windows/send.d.ts +6 -11
- package/dist/windows/send.js +70 -79
- package/dist/windows/send.js.map +1 -1
- package/dist/windows/signatures.d.ts +4 -10
- package/dist/windows/signatures.js +30 -37
- package/dist/windows/signatures.js.map +1 -1
- package/dist/windows/templates.d.ts +12 -18
- package/dist/windows/templates.js +126 -151
- package/dist/windows/templates.js.map +1 -1
- package/package.json +5 -4
- package/dist/OutlookService.d.ts +0 -43
- package/dist/OutlookService.js +0 -137
- package/dist/OutlookService.js.map +0 -1
- package/dist/outlookTemplateSections.d.ts +0 -62
- package/dist/outlookTemplateSections.js.map +0 -1
- package/dist/shared/capabilities.d.ts +0 -3
- package/dist/shared/capabilities.js +0 -9
- package/dist/shared/capabilities.js.map +0 -1
|
@@ -1,30 +1,35 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// happily returns a DIFFERENT message when the id is stale or wrong — common
|
|
6
|
-
// precisely when many replies share one subject — so a bare "not found" points
|
|
7
|
-
// the caller at the wrong problem. Naming the email actually resolved, and the
|
|
8
|
-
// attachments it does carry, is what makes the mistake visible.
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
9
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MAX_FILE_STEM = exports.RESERVED_FILE_NAME = exports.UNSAFE_FILE_NAME_CHARS = void 0;
|
|
10
7
|
exports.normalizeAttachmentName = normalizeAttachmentName;
|
|
11
8
|
exports.findAttachmentIndex = findAttachmentIndex;
|
|
12
9
|
exports.attachmentNotFoundMessage = attachmentNotFoundMessage;
|
|
10
|
+
exports.safeFileName = safeFileName;
|
|
11
|
+
exports.uniqueSavePath = uniqueSavePath;
|
|
12
|
+
// Matching an attachment by name, the error when nothing matches, and the
|
|
13
|
+
// file name it is saved under.
|
|
14
|
+
//
|
|
15
|
+
// The error is the useful half. Resolving an email by id can return a
|
|
16
|
+
// DIFFERENT message when the id is stale or wrong — common exactly when many
|
|
17
|
+
// replies share one subject — so a bare "not found" points at the wrong
|
|
18
|
+
// problem. Naming the email that actually resolved, and the attachments it does
|
|
19
|
+
// carry, is what makes the mistake visible.
|
|
20
|
+
const path_1 = __importDefault(require("path"));
|
|
13
21
|
/**
|
|
14
22
|
* Collapse whitespace runs (non-breaking spaces included) and trim, so a
|
|
15
|
-
* trivially reformatted
|
|
16
|
-
*
|
|
17
|
-
* likely to have substituted.
|
|
23
|
+
* trivially reformatted file name still resolves. Both platforms' regex engines
|
|
24
|
+
* count U+00A0 as whitespace.
|
|
18
25
|
*/
|
|
19
26
|
function normalizeAttachmentName(name) {
|
|
20
27
|
return name.replace(/\s+/g, ' ').trim();
|
|
21
28
|
}
|
|
22
29
|
/**
|
|
23
|
-
* Index of the attachment matching `wanted`, or -1
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* so an email carrying two names differing only in case still resolves the one
|
|
27
|
-
* that was actually asked for.
|
|
30
|
+
* Index of the attachment matching `wanted`, or -1: exact first, then
|
|
31
|
+
* whitespace-normalized and case-insensitive — in that order, so two names that
|
|
32
|
+
* differ only in case still resolve the one asked for.
|
|
28
33
|
*/
|
|
29
34
|
function findAttachmentIndex(names, wanted) {
|
|
30
35
|
const exact = names.indexOf(wanted);
|
|
@@ -34,17 +39,50 @@ function findAttachmentIndex(names, wanted) {
|
|
|
34
39
|
return names.findIndex(name => normalizeAttachmentName(name).toLowerCase() === normalized);
|
|
35
40
|
}
|
|
36
41
|
/**
|
|
37
|
-
* The "attachment not found" message, worded once for both platforms.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* build the identical sentence: it passes PowerShell interpolations
|
|
41
|
-
* (`$($item.Subject)`) where macOS passes the values it read.
|
|
42
|
+
* The "attachment not found" message, worded once for both platforms. Fields
|
|
43
|
+
* arrive as strings so the Windows script can build the same sentence from
|
|
44
|
+
* PowerShell variables where macOS passes the values it read.
|
|
42
45
|
*/
|
|
43
46
|
function attachmentNotFoundMessage(init) {
|
|
44
47
|
return `Attachment '${init.fileName}' not found. `
|
|
45
48
|
+ `Resolved email: from=${init.senderEmail}; subject=${init.subject}; received=${init.receivedTime}. `
|
|
46
49
|
+ `Attachments present: ${init.present}. `
|
|
47
|
-
+
|
|
48
|
-
|
|
50
|
+
+ 'If this is not the email you expected, its id is likely wrong or stale - list the mail again for a current one.';
|
|
51
|
+
}
|
|
52
|
+
/** Characters no file name may carry on Windows (a superset of macOS's). */
|
|
53
|
+
exports.UNSAFE_FILE_NAME_CHARS = '[<>:"/\\\\|?*\\x00-\\x1F]';
|
|
54
|
+
/** Names Windows reserves for devices, with or without an extension. */
|
|
55
|
+
exports.RESERVED_FILE_NAME = '^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\\..*)?$';
|
|
56
|
+
/** How long a saved name's stem may run before it is cut. */
|
|
57
|
+
exports.MAX_FILE_STEM = 150;
|
|
58
|
+
/**
|
|
59
|
+
* A sender-chosen attachment name made safe to write: no path separators or
|
|
60
|
+
* characters a file system rejects, no trailing dots or spaces, no reserved
|
|
61
|
+
* device name, and a stem short enough to stay clear of path-length limits. A
|
|
62
|
+
* name like `..\..\x.exe` therefore lands inside the destination as
|
|
63
|
+
* `.._.._x.exe`, never outside it.
|
|
64
|
+
*/
|
|
65
|
+
function safeFileName(name) {
|
|
66
|
+
let safe = name.replace(new RegExp(exports.UNSAFE_FILE_NAME_CHARS, 'g'), '_').replace(/[. ]+$/, '');
|
|
67
|
+
if (new RegExp(exports.RESERVED_FILE_NAME, 'i').test(safe))
|
|
68
|
+
safe = `_${safe}`;
|
|
69
|
+
const ext = path_1.default.extname(safe);
|
|
70
|
+
const stem = safe.slice(0, safe.length - ext.length);
|
|
71
|
+
if (stem.length > exports.MAX_FILE_STEM)
|
|
72
|
+
safe = stem.slice(0, exports.MAX_FILE_STEM) + ext;
|
|
73
|
+
return safe || 'attachment';
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A path in `dir` for `name` that `exists` reports free: the name itself, else
|
|
77
|
+
* `name (1).ext`, `name (2).ext`, … Existing files are never overwritten.
|
|
78
|
+
*/
|
|
79
|
+
function uniqueSavePath(dir, name, exists) {
|
|
80
|
+
const ext = path_1.default.extname(name);
|
|
81
|
+
const stem = name.slice(0, name.length - ext.length);
|
|
82
|
+
let candidate = path_1.default.join(dir, name);
|
|
83
|
+
for (let n = 1; exists(candidate); n++) {
|
|
84
|
+
candidate = path_1.default.join(dir, `${stem} (${n})${ext}`);
|
|
85
|
+
}
|
|
86
|
+
return candidate;
|
|
49
87
|
}
|
|
50
88
|
//# sourceMappingURL=attachmentMatch.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachmentMatch.js","sourceRoot":"","sources":["../../src/shared/attachmentMatch.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"attachmentMatch.js","sourceRoot":"","sources":["../../src/shared/attachmentMatch.ts"],"names":[],"mappings":"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","sourcesContent":["// Matching an attachment by name, the error when nothing matches, and the\n// file name it is saved under.\n//\n// The error is the useful half. Resolving an email by id can return a\n// DIFFERENT message when the id is stale or wrong — common exactly when many\n// replies share one subject — so a bare \"not found\" points at the wrong\n// problem. Naming the email that actually resolved, and the attachments it does\n// carry, is what makes the mistake visible.\nimport path from 'path';\n\n/**\n * Collapse whitespace runs (non-breaking spaces included) and trim, so a\n * trivially reformatted file name still resolves. Both platforms' regex engines\n * count U+00A0 as whitespace.\n */\nexport function normalizeAttachmentName(name: string): string {\n return name.replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * Index of the attachment matching `wanted`, or -1: exact first, then\n * whitespace-normalized and case-insensitive — in that order, so two names that\n * differ only in case still resolve the one asked for.\n */\nexport function findAttachmentIndex(names: readonly string[], wanted: string): number {\n const exact = names.indexOf(wanted);\n if (exact !== -1) return exact;\n const normalized = normalizeAttachmentName(wanted).toLowerCase();\n return names.findIndex(name => normalizeAttachmentName(name).toLowerCase() === normalized);\n}\n\n/**\n * The \"attachment not found\" message, worded once for both platforms. Fields\n * arrive as strings so the Windows script can build the same sentence from\n * PowerShell variables where macOS passes the values it read.\n */\nexport function attachmentNotFoundMessage(init: {\n fileName: string;\n senderEmail: string;\n subject: string;\n receivedTime: string;\n /** The attachments the email does carry, joined for display. */\n present: string;\n}): string {\n return `Attachment '${init.fileName}' not found. `\n + `Resolved email: from=${init.senderEmail}; subject=${init.subject}; received=${init.receivedTime}. `\n + `Attachments present: ${init.present}. `\n + 'If this is not the email you expected, its id is likely wrong or stale - list the mail again for a current one.';\n}\n\n/** Characters no file name may carry on Windows (a superset of macOS's). */\nexport const UNSAFE_FILE_NAME_CHARS = '[<>:\"/\\\\\\\\|?*\\\\x00-\\\\x1F]';\n\n/** Names Windows reserves for devices, with or without an extension. */\nexport const RESERVED_FILE_NAME = '^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\\\\..*)?$';\n\n/** How long a saved name's stem may run before it is cut. */\nexport const MAX_FILE_STEM = 150;\n\n/**\n * A sender-chosen attachment name made safe to write: no path separators or\n * characters a file system rejects, no trailing dots or spaces, no reserved\n * device name, and a stem short enough to stay clear of path-length limits. A\n * name like `..\\..\\x.exe` therefore lands inside the destination as\n * `.._.._x.exe`, never outside it.\n */\nexport function safeFileName(name: string): string {\n let safe = name.replace(new RegExp(UNSAFE_FILE_NAME_CHARS, 'g'), '_').replace(/[. ]+$/, '');\n if (new RegExp(RESERVED_FILE_NAME, 'i').test(safe)) safe = `_${safe}`;\n const ext = path.extname(safe);\n const stem = safe.slice(0, safe.length - ext.length);\n if (stem.length > MAX_FILE_STEM) safe = stem.slice(0, MAX_FILE_STEM) + ext;\n return safe || 'attachment';\n}\n\n/**\n * A path in `dir` for `name` that `exists` reports free: the name itself, else\n * `name (1).ext`, `name (2).ext`, … Existing files are never overwritten.\n */\nexport function uniqueSavePath(dir: string, name: string, exists: (file: string) => boolean): string {\n const ext = path.extname(name);\n const stem = name.slice(0, name.length - ext.length);\n let candidate = path.join(dir, name);\n for (let n = 1; exists(candidate); n++) {\n candidate = path.join(dir, `${stem} (${n})${ext}`);\n }\n return candidate;\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BounceReport, SentRecipientGroup } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Every sent message with a bounced recipient, newest first, and the bounced
|
|
4
|
+
* addresses no sent message accounts for.
|
|
5
|
+
*
|
|
6
|
+
* Kept per message rather than per address or domain: a message's recipient
|
|
7
|
+
* set is the one grouping a mailbox actually records, and whether a failure
|
|
8
|
+
* means anything — one colleague gone, or nobody reached at all — depends on
|
|
9
|
+
* what else that same message went to.
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildBounceReport(account: string, scannedDays: number, bouncedAddresses: readonly string[], sent: readonly SentRecipientGroup[]): BounceReport;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildBounceReport = buildBounceReport;
|
|
4
|
+
function distinctLowerCase(addresses) {
|
|
5
|
+
return [...new Set(addresses.map(address => address.trim().toLowerCase()).filter(Boolean))];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Every sent message with a bounced recipient, newest first, and the bounced
|
|
9
|
+
* addresses no sent message accounts for.
|
|
10
|
+
*
|
|
11
|
+
* Kept per message rather than per address or domain: a message's recipient
|
|
12
|
+
* set is the one grouping a mailbox actually records, and whether a failure
|
|
13
|
+
* means anything — one colleague gone, or nobody reached at all — depends on
|
|
14
|
+
* what else that same message went to.
|
|
15
|
+
*/
|
|
16
|
+
function buildBounceReport(account, scannedDays, bouncedAddresses, sent) {
|
|
17
|
+
const bounced = distinctLowerCase(bouncedAddresses);
|
|
18
|
+
const bouncedSet = new Set(bounced);
|
|
19
|
+
const matched = new Set();
|
|
20
|
+
const sends = [];
|
|
21
|
+
const newestFirst = [...sent].sort((a, b) => b.sentOn.localeCompare(a.sentOn));
|
|
22
|
+
for (const message of newestFirst) {
|
|
23
|
+
const recipients = distinctLowerCase(message.recipients);
|
|
24
|
+
const failedRecipients = recipients.filter(address => bouncedSet.has(address));
|
|
25
|
+
if (failedRecipients.length === 0)
|
|
26
|
+
continue;
|
|
27
|
+
failedRecipients.forEach(address => matched.add(address));
|
|
28
|
+
sends.push({
|
|
29
|
+
entryId: message.entryId,
|
|
30
|
+
subject: message.subject,
|
|
31
|
+
sentOn: message.sentOn,
|
|
32
|
+
recipients,
|
|
33
|
+
failedRecipients,
|
|
34
|
+
allFailed: failedRecipients.length === recipients.length,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
account,
|
|
39
|
+
scannedDays,
|
|
40
|
+
bouncedAddresses: bounced,
|
|
41
|
+
sends,
|
|
42
|
+
unmatchedAddresses: bounced.filter(address => !matched.has(address)).sort(),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=bounceReport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bounceReport.js","sourceRoot":"","sources":["../../src/shared/bounceReport.ts"],"names":[],"mappings":";;AAkBA,8CAkCC;AA/CD,SAAS,iBAAiB,CAAC,SAA4B;IACnD,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAChG,CAAC;AAED;;;;;;;;GAQG;AACH,SAAgB,iBAAiB,CAC7B,OAAe,EACf,WAAmB,EACnB,gBAAmC,EACnC,IAAmC;IAEnC,MAAM,OAAO,GAAG,iBAAiB,CAAC,gBAAgB,CAAC,CAAC;IACpD,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IACpC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,MAAM,KAAK,GAAkB,EAAE,CAAC;IAEhC,MAAM,WAAW,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/E,KAAK,MAAM,OAAO,IAAI,WAAW,EAAE,CAAC;QAChC,MAAM,UAAU,GAAG,iBAAiB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACzD,MAAM,gBAAgB,GAAG,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;QAC/E,IAAI,gBAAgB,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAC5C,gBAAgB,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;QAC1D,KAAK,CAAC,IAAI,CAAC;YACP,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,UAAU;YACV,gBAAgB;YAChB,SAAS,EAAE,gBAAgB,CAAC,MAAM,KAAK,UAAU,CAAC,MAAM;SAC3D,CAAC,CAAC;IACP,CAAC;IAED,OAAO;QACH,OAAO;QACP,WAAW;QACX,gBAAgB,EAAE,OAAO;QACzB,KAAK;QACL,kBAAkB,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE;KAC9E,CAAC;AACN,CAAC","sourcesContent":["// Measuring what was sent against what bounced. Both reads are platform work\n// (collectBouncedRecipients, readSentRecipientGroups); putting them side by side\n// is the same everywhere, so it is done once here.\nimport type { BouncedSend, BounceReport, SentRecipientGroup } from '../types';\n\nfunction distinctLowerCase(addresses: readonly string[]): string[] {\n return [...new Set(addresses.map(address => address.trim().toLowerCase()).filter(Boolean))];\n}\n\n/**\n * Every sent message with a bounced recipient, newest first, and the bounced\n * addresses no sent message accounts for.\n *\n * Kept per message rather than per address or domain: a message's recipient\n * set is the one grouping a mailbox actually records, and whether a failure\n * means anything — one colleague gone, or nobody reached at all — depends on\n * what else that same message went to.\n */\nexport function buildBounceReport(\n account: string,\n scannedDays: number,\n bouncedAddresses: readonly string[],\n sent: readonly SentRecipientGroup[],\n): BounceReport {\n const bounced = distinctLowerCase(bouncedAddresses);\n const bouncedSet = new Set(bounced);\n const matched = new Set<string>();\n const sends: BouncedSend[] = [];\n\n const newestFirst = [...sent].sort((a, b) => b.sentOn.localeCompare(a.sentOn));\n for (const message of newestFirst) {\n const recipients = distinctLowerCase(message.recipients);\n const failedRecipients = recipients.filter(address => bouncedSet.has(address));\n if (failedRecipients.length === 0) continue;\n failedRecipients.forEach(address => matched.add(address));\n sends.push({\n entryId: message.entryId,\n subject: message.subject,\n sentOn: message.sentOn,\n recipients,\n failedRecipients,\n allFailed: failedRecipients.length === recipients.length,\n });\n }\n\n return {\n account,\n scannedDays,\n bouncedAddresses: bounced,\n sends,\n unmatchedAddresses: bounced.filter(address => !matched.has(address)).sort(),\n };\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Subject fragments
|
|
1
|
+
/** Subject fragments only a mail system writes. Matched case-insensitively. */
|
|
2
2
|
export declare const BOUNCE_SUBJECT_PHRASES: readonly string[];
|
|
3
3
|
/** Fragments of a sender ADDRESS that fingerprint a mail-delivery daemon. */
|
|
4
4
|
export declare const BOUNCE_DAEMON_ADDRESSES: readonly string[];
|
|
@@ -6,28 +6,56 @@ export declare const BOUNCE_DAEMON_ADDRESSES: readonly string[];
|
|
|
6
6
|
export declare const BOUNCE_DAEMON_NAMES: readonly string[];
|
|
7
7
|
/** MessageClass prefix Exchange stamps on a non-delivery report. */
|
|
8
8
|
export declare const NDR_MESSAGE_CLASS_PREFIX = "REPORT.IPM.Note.NDR";
|
|
9
|
+
/**
|
|
10
|
+
* The same structural signal as it arrives on the wire, for macOS: a bounce is
|
|
11
|
+
* a delivery-status notification (RFC 3464) and says so in its own headers.
|
|
12
|
+
* Both markers must be present — `multipart/report` also carries read receipts,
|
|
13
|
+
* and it is the report type that separates a delivery that FAILED from one that
|
|
14
|
+
* was acknowledged.
|
|
15
|
+
*
|
|
16
|
+
* Outlook for Mac publishes no MessageClass in its AppleScript dictionary, so
|
|
17
|
+
* this stands in for it: the message Exchange stamps REPORT.IPM.Note.NDR is the
|
|
18
|
+
* message that arrived carrying these headers.
|
|
19
|
+
*
|
|
20
|
+
* The macOS index tests these in AppleScript, whose `contains` is already
|
|
21
|
+
* case-insensitive, and returns one flag per message rather than the headers
|
|
22
|
+
* themselves (see mac/bounces.ts).
|
|
23
|
+
*/
|
|
24
|
+
export declare const DSN_HEADER_MARKERS: readonly string[];
|
|
25
|
+
/** Address fragments never reported as a failed recipient — they are the bounce's own author. */
|
|
26
|
+
export declare const DAEMON_SELF_ADDRESSES: readonly string[];
|
|
27
|
+
/** At most this many failed recipients per bounce; a list bounce can name hundreds. */
|
|
28
|
+
export declare const MAX_FAILED_RECIPIENTS = 5;
|
|
29
|
+
/** The reasons a bounce is flagged, worded once. */
|
|
30
|
+
export declare const BOUNCE_REASON: {
|
|
31
|
+
readonly ndr: "Non-delivery report (NDR)";
|
|
32
|
+
readonly daemon: "From mail-delivery system";
|
|
33
|
+
readonly subjectPhrase: (phrase: string) => string;
|
|
34
|
+
};
|
|
9
35
|
/**
|
|
10
36
|
* Why this item is a bounce, or '' if it isn't.
|
|
11
37
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* first one that happened to be tested.
|
|
38
|
+
* The structural signal (a non-delivery report) outranks the sender
|
|
39
|
+
* fingerprint, which outranks a subject phrase — so the reason reported is the
|
|
40
|
+
* strongest one that applied, not whichever was tested first.
|
|
16
41
|
*/
|
|
17
42
|
export declare function bounceReason(item: {
|
|
18
43
|
subject?: string;
|
|
19
44
|
senderName?: string;
|
|
20
45
|
senderEmail?: string;
|
|
21
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* True when the item is STRUCTURALLY a non-delivery report, whichever way
|
|
48
|
+
* its platform can see that: its MessageClass on Windows, the
|
|
49
|
+
* delivery-status headers it arrived with on macOS.
|
|
50
|
+
*/
|
|
22
51
|
isNonDeliveryReport?: boolean;
|
|
23
52
|
}): string;
|
|
24
53
|
/** The address pattern both platforms scan a bounce body with. */
|
|
25
54
|
export declare const BODY_ADDRESS_PATTERN = "[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}";
|
|
26
55
|
/**
|
|
27
56
|
* The addresses a bounce body names as having failed — best effort, since a
|
|
28
|
-
* bounce body is prose with no agreed structure.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* every bounce, and neither is a send that failed.
|
|
57
|
+
* bounce body is prose with no agreed structure. The account's own address and
|
|
58
|
+
* the daemon's are dropped: both appear in nearly every bounce and neither is a
|
|
59
|
+
* send that failed.
|
|
32
60
|
*/
|
|
33
61
|
export declare function failedRecipients(body: string, accountAddress: string): string[];
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// What counts as a bounce, defined once for both platforms.
|
|
3
3
|
//
|
|
4
|
-
// Windows classifies inside the generated PowerShell
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// Windows classifies inside the generated PowerShell — an item's body is only
|
|
5
|
+
// read once a cheaper signal has matched, which keeps a large-inbox scan
|
|
6
|
+
// affordable — while macOS classifies here after a bulk property read. Two
|
|
7
|
+
// implementations of the RULES is how "what is a bounce" comes to mean two
|
|
8
|
+
// things, so every list and limit lives here and the PowerShell is generated
|
|
9
|
+
// from them (see windows/bounces.ts).
|
|
10
10
|
//
|
|
11
|
-
// Deliberately specific,
|
|
12
|
-
//
|
|
13
|
-
//
|
|
11
|
+
// Deliberately specific, mail-system wording: ordinary mail that merely
|
|
12
|
+
// mentions a "delivery" must never match, because the next step is often to
|
|
13
|
+
// delete what did.
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.BODY_ADDRESS_PATTERN = exports.NDR_MESSAGE_CLASS_PREFIX = exports.BOUNCE_DAEMON_NAMES = exports.BOUNCE_DAEMON_ADDRESSES = exports.BOUNCE_SUBJECT_PHRASES = void 0;
|
|
15
|
+
exports.BODY_ADDRESS_PATTERN = exports.BOUNCE_REASON = exports.MAX_FAILED_RECIPIENTS = exports.DAEMON_SELF_ADDRESSES = exports.DSN_HEADER_MARKERS = exports.NDR_MESSAGE_CLASS_PREFIX = exports.BOUNCE_DAEMON_NAMES = exports.BOUNCE_DAEMON_ADDRESSES = exports.BOUNCE_SUBJECT_PHRASES = void 0;
|
|
16
16
|
exports.bounceReason = bounceReason;
|
|
17
17
|
exports.failedRecipients = failedRecipients;
|
|
18
|
-
/** Subject fragments
|
|
18
|
+
/** Subject fragments only a mail system writes. Matched case-insensitively. */
|
|
19
19
|
exports.BOUNCE_SUBJECT_PHRASES = [
|
|
20
20
|
'undeliverable',
|
|
21
21
|
'message blocked',
|
|
@@ -46,41 +46,59 @@ exports.BOUNCE_DAEMON_NAMES = [
|
|
|
46
46
|
];
|
|
47
47
|
/** MessageClass prefix Exchange stamps on a non-delivery report. */
|
|
48
48
|
exports.NDR_MESSAGE_CLASS_PREFIX = 'REPORT.IPM.Note.NDR';
|
|
49
|
-
/**
|
|
50
|
-
|
|
49
|
+
/**
|
|
50
|
+
* The same structural signal as it arrives on the wire, for macOS: a bounce is
|
|
51
|
+
* a delivery-status notification (RFC 3464) and says so in its own headers.
|
|
52
|
+
* Both markers must be present — `multipart/report` also carries read receipts,
|
|
53
|
+
* and it is the report type that separates a delivery that FAILED from one that
|
|
54
|
+
* was acknowledged.
|
|
55
|
+
*
|
|
56
|
+
* Outlook for Mac publishes no MessageClass in its AppleScript dictionary, so
|
|
57
|
+
* this stands in for it: the message Exchange stamps REPORT.IPM.Note.NDR is the
|
|
58
|
+
* message that arrived carrying these headers.
|
|
59
|
+
*
|
|
60
|
+
* The macOS index tests these in AppleScript, whose `contains` is already
|
|
61
|
+
* case-insensitive, and returns one flag per message rather than the headers
|
|
62
|
+
* themselves (see mac/bounces.ts).
|
|
63
|
+
*/
|
|
64
|
+
exports.DSN_HEADER_MARKERS = ['multipart/report', 'delivery-status'];
|
|
65
|
+
/** Address fragments never reported as a failed recipient — they are the bounce's own author. */
|
|
66
|
+
exports.DAEMON_SELF_ADDRESSES = ['mailer-daemon', 'postmaster', 'mail-daemon'];
|
|
51
67
|
/** At most this many failed recipients per bounce; a list bounce can name hundreds. */
|
|
52
|
-
|
|
68
|
+
exports.MAX_FAILED_RECIPIENTS = 5;
|
|
69
|
+
/** The reasons a bounce is flagged, worded once. */
|
|
70
|
+
exports.BOUNCE_REASON = {
|
|
71
|
+
ndr: 'Non-delivery report (NDR)',
|
|
72
|
+
daemon: 'From mail-delivery system',
|
|
73
|
+
subjectPhrase: (phrase) => `Bounce subject phrase: '${phrase}'`,
|
|
74
|
+
};
|
|
53
75
|
/**
|
|
54
76
|
* Why this item is a bounce, or '' if it isn't.
|
|
55
77
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* first one that happened to be tested.
|
|
78
|
+
* The structural signal (a non-delivery report) outranks the sender
|
|
79
|
+
* fingerprint, which outranks a subject phrase — so the reason reported is the
|
|
80
|
+
* strongest one that applied, not whichever was tested first.
|
|
60
81
|
*/
|
|
61
82
|
function bounceReason(item) {
|
|
62
83
|
if (item.isNonDeliveryReport)
|
|
63
|
-
return
|
|
84
|
+
return exports.BOUNCE_REASON.ndr;
|
|
64
85
|
const senderEmail = (item.senderEmail || '').toLowerCase();
|
|
65
|
-
if (exports.BOUNCE_DAEMON_ADDRESSES.some(fragment => senderEmail.includes(fragment)))
|
|
66
|
-
return
|
|
67
|
-
}
|
|
86
|
+
if (exports.BOUNCE_DAEMON_ADDRESSES.some(fragment => senderEmail.includes(fragment)))
|
|
87
|
+
return exports.BOUNCE_REASON.daemon;
|
|
68
88
|
const senderName = (item.senderName || '').toLowerCase();
|
|
69
|
-
if (exports.BOUNCE_DAEMON_NAMES.some(fragment => senderName.includes(fragment)))
|
|
70
|
-
return
|
|
71
|
-
}
|
|
89
|
+
if (exports.BOUNCE_DAEMON_NAMES.some(fragment => senderName.includes(fragment)))
|
|
90
|
+
return exports.BOUNCE_REASON.daemon;
|
|
72
91
|
const subject = (item.subject || '').toLowerCase();
|
|
73
92
|
const phrase = exports.BOUNCE_SUBJECT_PHRASES.find(p => subject.includes(p));
|
|
74
|
-
return phrase ?
|
|
93
|
+
return phrase ? exports.BOUNCE_REASON.subjectPhrase(phrase) : '';
|
|
75
94
|
}
|
|
76
95
|
/** The address pattern both platforms scan a bounce body with. */
|
|
77
96
|
exports.BODY_ADDRESS_PATTERN = '[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}';
|
|
78
97
|
/**
|
|
79
98
|
* The addresses a bounce body names as having failed — best effort, since a
|
|
80
|
-
* bounce body is prose with no agreed structure.
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* every bounce, and neither is a send that failed.
|
|
99
|
+
* bounce body is prose with no agreed structure. The account's own address and
|
|
100
|
+
* the daemon's are dropped: both appear in nearly every bounce and neither is a
|
|
101
|
+
* send that failed.
|
|
84
102
|
*/
|
|
85
103
|
function failedRecipients(body, accountAddress) {
|
|
86
104
|
if (!body)
|
|
@@ -91,12 +109,12 @@ function failedRecipients(body, accountAddress) {
|
|
|
91
109
|
const address = match[0].toLowerCase().replace(/\.+$/, '');
|
|
92
110
|
if (address === target)
|
|
93
111
|
continue;
|
|
94
|
-
if (DAEMON_SELF_ADDRESSES.some(fragment => address.includes(fragment)))
|
|
112
|
+
if (exports.DAEMON_SELF_ADDRESSES.some(fragment => address.includes(fragment)))
|
|
95
113
|
continue;
|
|
96
114
|
if (found.includes(address))
|
|
97
115
|
continue;
|
|
98
116
|
found.push(address);
|
|
99
|
-
if (found.length >= MAX_FAILED_RECIPIENTS)
|
|
117
|
+
if (found.length >= exports.MAX_FAILED_RECIPIENTS)
|
|
100
118
|
break;
|
|
101
119
|
}
|
|
102
120
|
return found;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bounceRules.js","sourceRoot":"","sources":["../../src/shared/bounceRules.ts"],"names":[],"mappings":";AAAA,4DAA4D;AAC5D,EAAE;AACF,
|
|
1
|
+
{"version":3,"file":"bounceRules.js","sourceRoot":"","sources":["../../src/shared/bounceRules.ts"],"names":[],"mappings":"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","sourcesContent":["// What counts as a bounce, defined once for both platforms.\n//\n// Windows classifies inside the generated PowerShell — an item's body is only\n// read once a cheaper signal has matched, which keeps a large-inbox scan\n// affordable — while macOS classifies here after a bulk property read. Two\n// implementations of the RULES is how \"what is a bounce\" comes to mean two\n// things, so every list and limit lives here and the PowerShell is generated\n// from them (see windows/bounces.ts).\n//\n// Deliberately specific, mail-system wording: ordinary mail that merely\n// mentions a \"delivery\" must never match, because the next step is often to\n// delete what did.\n\n/** Subject fragments only a mail system writes. Matched case-insensitively. */\nexport const BOUNCE_SUBJECT_PHRASES: readonly string[] = [\n 'undeliverable',\n 'message blocked',\n 'mail delivery failed',\n 'delivery status notification (failure)',\n 'delivery has failed',\n 'failure notice',\n 'undelivered mail returned to sender',\n 'returned mail',\n];\n\n/** Fragments of a sender ADDRESS that fingerprint a mail-delivery daemon. */\nexport const BOUNCE_DAEMON_ADDRESSES: readonly string[] = [\n 'mailer-daemon',\n 'mailer_daemon',\n 'postmaster@',\n 'mail-daemon',\n 'maildelivery',\n];\n\n/** Fragments of a sender DISPLAY NAME that fingerprint a mail-delivery daemon. */\nexport const BOUNCE_DAEMON_NAMES: readonly string[] = [\n 'mail delivery subsystem',\n 'mail delivery system',\n 'microsoft outlook',\n 'postmaster',\n 'mailer-daemon',\n 'mail administrator',\n 'internet mail delivery',\n];\n\n/** MessageClass prefix Exchange stamps on a non-delivery report. */\nexport const NDR_MESSAGE_CLASS_PREFIX = 'REPORT.IPM.Note.NDR';\n\n/**\n * The same structural signal as it arrives on the wire, for macOS: a bounce is\n * a delivery-status notification (RFC 3464) and says so in its own headers.\n * Both markers must be present — `multipart/report` also carries read receipts,\n * and it is the report type that separates a delivery that FAILED from one that\n * was acknowledged.\n *\n * Outlook for Mac publishes no MessageClass in its AppleScript dictionary, so\n * this stands in for it: the message Exchange stamps REPORT.IPM.Note.NDR is the\n * message that arrived carrying these headers.\n *\n * The macOS index tests these in AppleScript, whose `contains` is already\n * case-insensitive, and returns one flag per message rather than the headers\n * themselves (see mac/bounces.ts).\n */\nexport const DSN_HEADER_MARKERS: readonly string[] = ['multipart/report', 'delivery-status'];\n\n/** Address fragments never reported as a failed recipient — they are the bounce's own author. */\nexport const DAEMON_SELF_ADDRESSES: readonly string[] = ['mailer-daemon', 'postmaster', 'mail-daemon'];\n\n/** At most this many failed recipients per bounce; a list bounce can name hundreds. */\nexport const MAX_FAILED_RECIPIENTS = 5;\n\n/** The reasons a bounce is flagged, worded once. */\nexport const BOUNCE_REASON = {\n ndr: 'Non-delivery report (NDR)',\n daemon: 'From mail-delivery system',\n subjectPhrase: (phrase: string) => `Bounce subject phrase: '${phrase}'`,\n} as const;\n\n/**\n * Why this item is a bounce, or '' if it isn't.\n *\n * The structural signal (a non-delivery report) outranks the sender\n * fingerprint, which outranks a subject phrase — so the reason reported is the\n * strongest one that applied, not whichever was tested first.\n */\nexport function bounceReason(item: {\n subject?: string;\n senderName?: string;\n senderEmail?: string;\n /**\n * True when the item is STRUCTURALLY a non-delivery report, whichever way\n * its platform can see that: its MessageClass on Windows, the\n * delivery-status headers it arrived with on macOS.\n */\n isNonDeliveryReport?: boolean;\n}): string {\n if (item.isNonDeliveryReport) return BOUNCE_REASON.ndr;\n\n const senderEmail = (item.senderEmail || '').toLowerCase();\n if (BOUNCE_DAEMON_ADDRESSES.some(fragment => senderEmail.includes(fragment))) return BOUNCE_REASON.daemon;\n const senderName = (item.senderName || '').toLowerCase();\n if (BOUNCE_DAEMON_NAMES.some(fragment => senderName.includes(fragment))) return BOUNCE_REASON.daemon;\n const subject = (item.subject || '').toLowerCase();\n const phrase = BOUNCE_SUBJECT_PHRASES.find(p => subject.includes(p));\n return phrase ? BOUNCE_REASON.subjectPhrase(phrase) : '';\n}\n\n/** The address pattern both platforms scan a bounce body with. */\nexport const BODY_ADDRESS_PATTERN = '[A-Za-z0-9._%+\\\\-]+@[A-Za-z0-9.\\\\-]+\\\\.[A-Za-z]{2,}';\n\n/**\n * The addresses a bounce body names as having failed — best effort, since a\n * bounce body is prose with no agreed structure. The account's own address and\n * the daemon's are dropped: both appear in nearly every bounce and neither is a\n * send that failed.\n */\nexport function failedRecipients(body: string, accountAddress: string): string[] {\n if (!body) return [];\n const target = (accountAddress || '').toLowerCase();\n const found: string[] = [];\n for (const match of body.matchAll(new RegExp(BODY_ADDRESS_PATTERN, 'g'))) {\n const address = match[0].toLowerCase().replace(/\\.+$/, '');\n if (address === target) continue;\n if (DAEMON_SELF_ADDRESSES.some(fragment => address.includes(fragment))) continue;\n if (found.includes(address)) continue;\n found.push(address);\n if (found.length >= MAX_FAILED_RECIPIENTS) break;\n }\n return found;\n}\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ScriptRunner } from '../errors';
|
|
2
|
+
import { type RunBudget } from '../runtime';
|
|
3
|
+
export interface ScriptProcess {
|
|
4
|
+
runner: ScriptRunner;
|
|
5
|
+
command: string;
|
|
6
|
+
args: readonly string[];
|
|
7
|
+
/** The script exactly as it runs, for errors and the debug hook. */
|
|
8
|
+
script: string;
|
|
9
|
+
budget: RunBudget;
|
|
10
|
+
/** The script's own failure message, and the line it came from, out of stderr. */
|
|
11
|
+
parseFailure(stderr: string): {
|
|
12
|
+
message: string;
|
|
13
|
+
line?: number;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface ProcessOutput {
|
|
17
|
+
stdout: string;
|
|
18
|
+
durationMs: number;
|
|
19
|
+
}
|
|
20
|
+
/** Run one script to completion, rejecting with the most specific OutlookError on failure. */
|
|
21
|
+
export declare function runScriptProcess(run: ScriptProcess): Promise<ProcessOutput>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runScriptProcess = runScriptProcess;
|
|
4
|
+
// Running one interpreter process: the part both platforms' runners share.
|
|
5
|
+
//
|
|
6
|
+
// Each platform decides how its script reaches the interpreter and how a
|
|
7
|
+
// failure reads in that interpreter's stderr. Everything else — the settings a
|
|
8
|
+
// run takes, telling an abort from a timeout from an overflow, and reporting
|
|
9
|
+
// the run to the debug hook — happens here, once.
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
const errors_1 = require("../errors");
|
|
12
|
+
const runtime_1 = require("../runtime");
|
|
13
|
+
/** Run one script to completion, rejecting with the most specific OutlookError on failure. */
|
|
14
|
+
function runScriptProcess(run) {
|
|
15
|
+
const { maxBufferBytes, signal } = (0, runtime_1.getConfig)();
|
|
16
|
+
const timeout = (0, runtime_1.timeoutFor)(run.budget);
|
|
17
|
+
const startedAt = Date.now();
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
(0, child_process_1.execFile)(run.command, [...run.args],
|
|
20
|
+
// windowsHide keeps a GUI host (an Electron app, say) from flashing a
|
|
21
|
+
// console window on every run.
|
|
22
|
+
{ maxBuffer: maxBufferBytes, timeout, signal, windowsHide: true, encoding: 'utf8' }, (error, stdout, stderr) => {
|
|
23
|
+
const durationMs = Date.now() - startedAt;
|
|
24
|
+
if (error) {
|
|
25
|
+
const { message, line } = run.parseFailure(stderr ?? '');
|
|
26
|
+
const failure = (0, errors_1.classifyRunFailure)({
|
|
27
|
+
runner: run.runner,
|
|
28
|
+
script: run.script,
|
|
29
|
+
stderr: stderr ?? '',
|
|
30
|
+
message,
|
|
31
|
+
line,
|
|
32
|
+
durationMs,
|
|
33
|
+
nodeError: error,
|
|
34
|
+
timeoutMs: timeout,
|
|
35
|
+
maxBufferBytes,
|
|
36
|
+
signal,
|
|
37
|
+
});
|
|
38
|
+
(0, runtime_1.reportRun)({ runner: run.runner, script: run.script, durationMs, error: failure.message });
|
|
39
|
+
reject(failure);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
(0, runtime_1.reportRun)({ runner: run.runner, script: run.script, durationMs });
|
|
43
|
+
resolve({ stdout, durationMs });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=exec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/shared/exec.ts"],"names":[],"mappings":";;AA4BA,4CAoCC;AAhED,2EAA2E;AAC3E,EAAE;AACF,yEAAyE;AACzE,+EAA+E;AAC/E,6EAA6E;AAC7E,kDAAkD;AAClD,iDAAyC;AACzC,sCAAkE;AAClE,wCAA8E;AAmB9E,8FAA8F;AAC9F,SAAgB,gBAAgB,CAAC,GAAkB;IAC/C,MAAM,EAAC,cAAc,EAAE,MAAM,EAAC,GAAG,IAAA,mBAAS,GAAE,CAAC;IAC7C,MAAM,OAAO,GAAG,IAAA,oBAAU,EAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACvC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACnC,IAAA,wBAAQ,EACJ,GAAG,CAAC,OAAO,EACX,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC;QACb,sEAAsE;QACtE,+BAA+B;QAC/B,EAAC,SAAS,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAC,EACjF,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACtB,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;YAC1C,IAAI,KAAK,EAAE,CAAC;gBACR,MAAM,EAAC,OAAO,EAAE,IAAI,EAAC,GAAG,GAAG,CAAC,YAAY,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;gBACvD,MAAM,OAAO,GAAG,IAAA,2BAAkB,EAAC;oBAC/B,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,MAAM,EAAE,MAAM,IAAI,EAAE;oBACpB,OAAO;oBACP,IAAI;oBACJ,UAAU;oBACV,SAAS,EAAE,KAAK;oBAChB,SAAS,EAAE,OAAO;oBAClB,cAAc;oBACd,MAAM;iBACT,CAAC,CAAC;gBACH,IAAA,mBAAS,EAAC,EAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,EAAC,CAAC,CAAC;gBACxF,MAAM,CAAC,OAAO,CAAC,CAAC;gBAChB,OAAO;YACX,CAAC;YACD,IAAA,mBAAS,EAAC,EAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,EAAC,CAAC,CAAC;YAChE,OAAO,CAAC,EAAC,MAAM,EAAE,UAAU,EAAC,CAAC,CAAC;QAClC,CAAC,CACJ,CAAC;IACN,CAAC,CAAC,CAAC;AACP,CAAC","sourcesContent":["// Running one interpreter process: the part both platforms' runners share.\n//\n// Each platform decides how its script reaches the interpreter and how a\n// failure reads in that interpreter's stderr. Everything else — the settings a\n// run takes, telling an abort from a timeout from an overflow, and reporting\n// the run to the debug hook — happens here, once.\nimport { execFile } from 'child_process';\nimport { classifyRunFailure, type ScriptRunner } from '../errors';\nimport { getConfig, reportRun, type RunBudget, timeoutFor } from '../runtime';\n\nexport interface ScriptProcess {\n runner: ScriptRunner;\n command: string;\n args: readonly string[];\n /** The script exactly as it runs, for errors and the debug hook. */\n script: string;\n budget: RunBudget;\n\n /** The script's own failure message, and the line it came from, out of stderr. */\n parseFailure(stderr: string): { message: string; line?: number };\n}\n\nexport interface ProcessOutput {\n stdout: string;\n durationMs: number;\n}\n\n/** Run one script to completion, rejecting with the most specific OutlookError on failure. */\nexport function runScriptProcess(run: ScriptProcess): Promise<ProcessOutput> {\n const {maxBufferBytes, signal} = getConfig();\n const timeout = timeoutFor(run.budget);\n const startedAt = Date.now();\n return new Promise((resolve, reject) => {\n execFile(\n run.command,\n [...run.args],\n // windowsHide keeps a GUI host (an Electron app, say) from flashing a\n // console window on every run.\n {maxBuffer: maxBufferBytes, timeout, signal, windowsHide: true, encoding: 'utf8'},\n (error, stdout, stderr) => {\n const durationMs = Date.now() - startedAt;\n if (error) {\n const {message, line} = run.parseFailure(stderr ?? '');\n const failure = classifyRunFailure({\n runner: run.runner,\n script: run.script,\n stderr: stderr ?? '',\n message,\n line,\n durationMs,\n nodeError: error,\n timeoutMs: timeout,\n maxBufferBytes,\n signal,\n });\n reportRun({runner: run.runner, script: run.script, durationMs, error: failure.message});\n reject(failure);\n return;\n }\n reportRun({runner: run.runner, script: run.script, durationMs});\n resolve({stdout, durationMs});\n },\n );\n });\n}\n"]}
|
package/dist/shared/json.d.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export declare function parseObject(raw: string): Record<string, unknown>;
|
|
3
|
-
/** Parse a script's stdout as a list, absorbing ConvertTo-Json's unwrapping. */
|
|
4
|
-
export declare function parseArray(raw: string): unknown[];
|
|
5
|
-
/** One value as a list: an array stays, null becomes empty, anything else wraps. */
|
|
1
|
+
/** One value as a list: an array stays, null and undefined become empty, anything else wraps. */
|
|
6
2
|
export declare function toArray(value: unknown): unknown[];
|
|
7
|
-
/** A
|
|
3
|
+
/** A value as a plain object, for reading fields off one row. */
|
|
8
4
|
export declare function record(value: unknown): Record<string, unknown>;
|
|
9
5
|
export declare function str(value: unknown): string;
|
|
10
6
|
export declare function num(value: unknown): number;
|
|
7
|
+
export declare function bool(value: unknown): boolean;
|
|
8
|
+
/** A list of non-empty strings. */
|
|
11
9
|
export declare function strList(value: unknown): string[];
|
|
10
|
+
/** A base64 field decoded as UTF-8 — how scripts pass bodies that must survive intact. */
|
|
11
|
+
export declare function base64Text(value: unknown): string;
|
|
12
|
+
/** A list of per-item failures, as the scripts report them. */
|
|
13
|
+
export declare function itemFailures(value: unknown): {
|
|
14
|
+
entryId: string;
|
|
15
|
+
subject: string;
|
|
16
|
+
error: string;
|
|
17
|
+
}[];
|
package/dist/shared/json.js
CHANGED
|
@@ -1,45 +1,51 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
//
|
|
2
|
+
// Coercing the JSON a PowerShell script prints into the package's types.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// ConvertTo-Json has two habits a caller must never see: it unwraps a
|
|
5
|
+
// single-element array into a bare object, and it renders an empty one as
|
|
6
|
+
// nothing at all. Every list crossing that boundary goes through `toArray`, and
|
|
7
|
+
// every scalar through a coercion that survives a missing property.
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.parseObject = parseObject;
|
|
10
|
-
exports.parseArray = parseArray;
|
|
11
9
|
exports.toArray = toArray;
|
|
12
10
|
exports.record = record;
|
|
13
11
|
exports.str = str;
|
|
14
12
|
exports.num = num;
|
|
13
|
+
exports.bool = bool;
|
|
15
14
|
exports.strList = strList;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return {};
|
|
20
|
-
return JSON.parse(raw.trim());
|
|
21
|
-
}
|
|
22
|
-
/** Parse a script's stdout as a list, absorbing ConvertTo-Json's unwrapping. */
|
|
23
|
-
function parseArray(raw) {
|
|
24
|
-
if (!raw || !raw.trim() || raw.trim() === 'null')
|
|
25
|
-
return [];
|
|
26
|
-
return toArray(JSON.parse(raw.trim()));
|
|
27
|
-
}
|
|
28
|
-
/** One value as a list: an array stays, null becomes empty, anything else wraps. */
|
|
15
|
+
exports.base64Text = base64Text;
|
|
16
|
+
exports.itemFailures = itemFailures;
|
|
17
|
+
/** One value as a list: an array stays, null and undefined become empty, anything else wraps. */
|
|
29
18
|
function toArray(value) {
|
|
30
19
|
return Array.isArray(value) ? value : value == null ? [] : [value];
|
|
31
20
|
}
|
|
32
|
-
/** A
|
|
21
|
+
/** A value as a plain object, for reading fields off one row. */
|
|
33
22
|
function record(value) {
|
|
34
|
-
return
|
|
23
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
24
|
+
? value
|
|
25
|
+
: {};
|
|
35
26
|
}
|
|
36
27
|
function str(value) {
|
|
37
28
|
return value == null ? '' : String(value);
|
|
38
29
|
}
|
|
39
30
|
function num(value) {
|
|
40
|
-
return typeof value === 'number' ? value : 0;
|
|
31
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
41
32
|
}
|
|
33
|
+
function bool(value) {
|
|
34
|
+
return value === true;
|
|
35
|
+
}
|
|
36
|
+
/** A list of non-empty strings. */
|
|
42
37
|
function strList(value) {
|
|
43
38
|
return toArray(value).map(str).filter(Boolean);
|
|
44
39
|
}
|
|
40
|
+
/** A base64 field decoded as UTF-8 — how scripts pass bodies that must survive intact. */
|
|
41
|
+
function base64Text(value) {
|
|
42
|
+
return Buffer.from(str(value), 'base64').toString('utf8');
|
|
43
|
+
}
|
|
44
|
+
/** A list of per-item failures, as the scripts report them. */
|
|
45
|
+
function itemFailures(value) {
|
|
46
|
+
return toArray(value).map(row => {
|
|
47
|
+
const e = record(row);
|
|
48
|
+
return { entryId: str(e.entryId), subject: str(e.subject), error: str(e.error) };
|
|
49
|
+
});
|
|
50
|
+
}
|
|
45
51
|
//# sourceMappingURL=json.js.map
|