@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.
Files changed (139) hide show
  1. package/README.md +150 -183
  2. package/dist/backend.d.ts +151 -0
  3. package/dist/backend.js +3 -0
  4. package/dist/backend.js.map +1 -0
  5. package/dist/bridge.d.ts +56 -0
  6. package/dist/bridge.js +507 -0
  7. package/dist/bridge.js.map +1 -0
  8. package/dist/errors.d.ts +81 -43
  9. package/dist/errors.js +98 -52
  10. package/dist/errors.js.map +1 -1
  11. package/dist/index.d.ts +10 -10
  12. package/dist/index.js +25 -52
  13. package/dist/index.js.map +1 -1
  14. package/dist/mac/accounts.d.ts +7 -2
  15. package/dist/mac/accounts.js +23 -6
  16. package/dist/mac/accounts.js.map +1 -1
  17. package/dist/mac/attachments.d.ts +5 -25
  18. package/dist/mac/attachments.js +40 -99
  19. package/dist/mac/attachments.js.map +1 -1
  20. package/dist/mac/bounces.d.ts +12 -27
  21. package/dist/mac/bounces.js +95 -112
  22. package/dist/mac/bounces.js.map +1 -1
  23. package/dist/mac/cleanup.d.ts +11 -25
  24. package/dist/mac/cleanup.js +50 -76
  25. package/dist/mac/cleanup.js.map +1 -1
  26. package/dist/mac/drafts.d.ts +13 -34
  27. package/dist/mac/drafts.js +95 -169
  28. package/dist/mac/drafts.js.map +1 -1
  29. package/dist/mac/folders.d.ts +10 -21
  30. package/dist/mac/folders.js +33 -47
  31. package/dist/mac/folders.js.map +1 -1
  32. package/dist/mac/index.d.ts +2 -22
  33. package/dist/mac/index.js +19 -81
  34. package/dist/mac/index.js.map +1 -1
  35. package/dist/mac/profile.d.ts +13 -0
  36. package/dist/mac/profile.js +149 -0
  37. package/dist/mac/profile.js.map +1 -0
  38. package/dist/mac/read.d.ts +18 -36
  39. package/dist/mac/read.js +154 -208
  40. package/dist/mac/read.js.map +1 -1
  41. package/dist/mac/run.d.ts +35 -39
  42. package/dist/mac/run.js +97 -102
  43. package/dist/mac/run.js.map +1 -1
  44. package/dist/mac/scripts.d.ts +78 -86
  45. package/dist/mac/scripts.js +125 -115
  46. package/dist/mac/scripts.js.map +1 -1
  47. package/dist/mac/send.d.ts +10 -20
  48. package/dist/mac/send.js +64 -86
  49. package/dist/mac/send.js.map +1 -1
  50. package/dist/mac/signatures.d.ts +2 -2
  51. package/dist/mac/signatures.js +9 -11
  52. package/dist/mac/signatures.js.map +1 -1
  53. package/dist/mac/templates.d.ts +14 -26
  54. package/dist/mac/templates.js +75 -113
  55. package/dist/mac/templates.js.map +1 -1
  56. package/dist/mail.d.ts +77 -45
  57. package/dist/mail.js +166 -73
  58. package/dist/mail.js.map +1 -1
  59. package/dist/runtime.d.ts +76 -43
  60. package/dist/runtime.js +103 -48
  61. package/dist/runtime.js.map +1 -1
  62. package/dist/shared/args.d.ts +48 -0
  63. package/dist/shared/args.js +216 -0
  64. package/dist/shared/args.js.map +1 -0
  65. package/dist/shared/attachmentMatch.d.ts +28 -16
  66. package/dist/shared/attachmentMatch.js +60 -22
  67. package/dist/shared/attachmentMatch.js.map +1 -1
  68. package/dist/shared/bounceReport.d.ts +11 -0
  69. package/dist/shared/bounceReport.js +45 -0
  70. package/dist/shared/bounceReport.js.map +1 -0
  71. package/dist/shared/bounceRules.d.ts +38 -10
  72. package/dist/shared/bounceRules.js +50 -32
  73. package/dist/shared/bounceRules.js.map +1 -1
  74. package/dist/shared/exec.d.ts +21 -0
  75. package/dist/shared/exec.js +47 -0
  76. package/dist/shared/exec.js.map +1 -0
  77. package/dist/shared/json.d.ts +12 -6
  78. package/dist/shared/json.js +29 -23
  79. package/dist/shared/json.js.map +1 -1
  80. package/dist/shared/locate.d.ts +39 -0
  81. package/dist/shared/locate.js +129 -0
  82. package/dist/shared/locate.js.map +1 -0
  83. package/dist/shared/replyBody.d.ts +42 -32
  84. package/dist/shared/replyBody.js +84 -71
  85. package/dist/shared/replyBody.js.map +1 -1
  86. package/dist/templateBody.d.ts +63 -0
  87. package/dist/{outlookTemplateSections.js → templateBody.js} +72 -72
  88. package/dist/templateBody.js.map +1 -0
  89. package/dist/types.d.ts +360 -165
  90. package/dist/types.js +8 -9
  91. package/dist/types.js.map +1 -1
  92. package/dist/windows/accounts.d.ts +10 -1
  93. package/dist/windows/accounts.js +25 -13
  94. package/dist/windows/accounts.js.map +1 -1
  95. package/dist/windows/attachments.d.ts +9 -24
  96. package/dist/windows/attachments.js +94 -119
  97. package/dist/windows/attachments.js.map +1 -1
  98. package/dist/windows/bounces.d.ts +13 -26
  99. package/dist/windows/bounces.js +169 -200
  100. package/dist/windows/bounces.js.map +1 -1
  101. package/dist/windows/cleanup.d.ts +19 -23
  102. package/dist/windows/cleanup.js +93 -111
  103. package/dist/windows/cleanup.js.map +1 -1
  104. package/dist/windows/drafts.d.ts +16 -35
  105. package/dist/windows/drafts.js +167 -253
  106. package/dist/windows/drafts.js.map +1 -1
  107. package/dist/windows/folders.d.ts +8 -13
  108. package/dist/windows/folders.js +45 -52
  109. package/dist/windows/folders.js.map +1 -1
  110. package/dist/windows/index.d.ts +2 -27
  111. package/dist/windows/index.js +11 -65
  112. package/dist/windows/index.js.map +1 -1
  113. package/dist/windows/read.d.ts +24 -45
  114. package/dist/windows/read.js +266 -279
  115. package/dist/windows/read.js.map +1 -1
  116. package/dist/windows/run.d.ts +39 -39
  117. package/dist/windows/run.js +151 -111
  118. package/dist/windows/run.js.map +1 -1
  119. package/dist/windows/scripts.d.ts +71 -38
  120. package/dist/windows/scripts.js +123 -71
  121. package/dist/windows/scripts.js.map +1 -1
  122. package/dist/windows/send.d.ts +6 -11
  123. package/dist/windows/send.js +70 -79
  124. package/dist/windows/send.js.map +1 -1
  125. package/dist/windows/signatures.d.ts +4 -10
  126. package/dist/windows/signatures.js +30 -37
  127. package/dist/windows/signatures.js.map +1 -1
  128. package/dist/windows/templates.d.ts +12 -18
  129. package/dist/windows/templates.js +126 -151
  130. package/dist/windows/templates.js.map +1 -1
  131. package/package.json +5 -4
  132. package/dist/OutlookService.d.ts +0 -43
  133. package/dist/OutlookService.js +0 -137
  134. package/dist/OutlookService.js.map +0 -1
  135. package/dist/outlookTemplateSections.d.ts +0 -62
  136. package/dist/outlookTemplateSections.js.map +0 -1
  137. package/dist/shared/capabilities.d.ts +0 -3
  138. package/dist/shared/capabilities.js +0 -9
  139. package/dist/shared/capabilities.js.map +0 -1
package/dist/runtime.js CHANGED
@@ -3,54 +3,57 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RUN_BUDGETS = void 0;
6
7
  exports.mergeOptions = mergeOptions;
7
8
  exports.configure = configure;
8
9
  exports.getConfig = getConfig;
9
10
  exports.getGlobalConfig = getGlobalConfig;
10
11
  exports.withConfig = withConfig;
12
+ exports.timeoutFor = timeoutFor;
11
13
  exports.reportRun = reportRun;
12
14
  exports.tempFile = tempFile;
13
- exports.makeAttachmentDir = makeAttachmentDir;
14
- exports.resolveDestDir = resolveDestDir;
15
- // Process-wide knobs and shared scratch-file handling for the generated
16
- // PowerShell / AppleScript runs.
15
+ exports.removeTempFile = removeTempFile;
16
+ exports.attachmentDestination = attachmentDestination;
17
+ // Settings, and the scratch files every run needs.
17
18
  //
18
- // Every call in this package works by generating a script and shelling out to an
19
- // interpreter, which leaves a consumer four things they cannot otherwise reach:
20
- // how long a run may take, how much it may print, how to call it off, and what
21
- // the script actually said when it failed. Those are the settings here.
19
+ // Each operation generates a script and runs it under an interpreter, which
20
+ // leaves a caller four things they cannot otherwise reach: how long a run may
21
+ // take, how much it may print, how to call it off, and what the script actually
22
+ // said. Those are the settings here — process-wide through `configure()`, or per
23
+ // bridge through `createOutlookBridge()`.
22
24
  const async_hooks_1 = require("async_hooks");
23
25
  const fs_1 = __importDefault(require("fs"));
24
26
  const os_1 = __importDefault(require("os"));
25
27
  const path_1 = __importDefault(require("path"));
28
+ const errors_1 = require("./errors");
26
29
  const envDebug = process.env.OUTLOOK_BRIDGE_DEBUG;
27
- /** The process-wide defaults, as mutated by `configure()`. */
28
- const globalConfig = {
29
- timeoutMs: 120_000,
30
+ /** The process-wide settings, as `configure()` leaves them. */
31
+ let globalConfig = {
32
+ timeoutMs: undefined,
30
33
  maxBufferBytes: 8 * 1024 * 1024,
31
34
  tempDir: os_1.default.tmpdir(),
35
+ signal: undefined,
32
36
  debug: !!envDebug && envDebug !== '0',
33
37
  };
34
38
  /**
35
- * Per-instance config, scoped to an async call tree.
39
+ * Per-bridge settings, scoped to an async call tree.
36
40
  *
37
- * `createOutlookBridge()` hands each instance its own settings, but the platform
38
- * services are 3,000 lines of module-level functions that read `getConfig()`
39
- * directly. Threading a config parameter through all of them would touch every
40
- * line for no behavioural gain; an AsyncLocalStorage lets an instance wrap its
41
- * calls instead, and the store follows across every `await` inside one. Two
42
- * bridges with different timeouts can then run concurrently in one process,
43
- * which is the thing a second consumer actually needs and the global could never
44
- * give them.
41
+ * The platform code reads its settings through `getConfig()` wherever it needs
42
+ * them rather than having a config threaded through every function. A bridge
43
+ * instance runs each call inside `withConfig`, and the store follows every
44
+ * `await` beneath it — so two bridges with different timeouts can run at once
45
+ * in one process without seeing each other's.
45
46
  */
46
47
  const scoped = new async_hooks_1.AsyncLocalStorage();
47
- /** Merge caller options over a base, ignoring the keys they left out. */
48
+ /** Caller options merged over a base; keys left out keep the base's value. */
48
49
  function mergeOptions(base, options = {}) {
49
50
  const next = { ...base };
50
- if (options.timeoutMs !== undefined)
51
- next.timeoutMs = Math.max(0, options.timeoutMs);
51
+ if (options.timeoutMs === null)
52
+ next.timeoutMs = undefined;
53
+ else if (options.timeoutMs !== undefined)
54
+ next.timeoutMs = nonNegative(options.timeoutMs, 'timeoutMs');
52
55
  if (options.maxBufferBytes !== undefined)
53
- next.maxBufferBytes = Math.max(1, options.maxBufferBytes);
56
+ next.maxBufferBytes = Math.max(1, nonNegative(options.maxBufferBytes, 'maxBufferBytes'));
54
57
  if (options.tempDir !== undefined)
55
58
  next.tempDir = options.tempDir;
56
59
  if (options.signal !== undefined)
@@ -59,21 +62,27 @@ function mergeOptions(base, options = {}) {
59
62
  next.debug = options.debug;
60
63
  return next;
61
64
  }
65
+ function nonNegative(value, name) {
66
+ if (typeof value !== 'number' || Number.isNaN(value)) {
67
+ throw new errors_1.InvalidRequestError(`${name} must be a number (got ${String(value)}).`);
68
+ }
69
+ return Math.max(0, Math.floor(value));
70
+ }
62
71
  /**
63
- * Override the process-wide defaults. Merges, so naming one option leaves the
64
- * rest alone. Call before the first automation call.
72
+ * Change the process-wide settings. Merges, so naming one option leaves the
73
+ * rest alone.
65
74
  *
66
- * Affects every caller in the process. Prefer `createOutlookBridge(options)` in
67
- * anything that shares a process with code you don't own.
75
+ * Affects every caller in the process that uses the exported functions. Prefer
76
+ * `createOutlookBridge(options)` in a process shared with code you don't own.
68
77
  */
69
78
  function configure(options) {
70
- Object.assign(globalConfig, mergeOptions(globalConfig, options));
79
+ globalConfig = mergeOptions(globalConfig, options);
71
80
  }
72
- /** The settings in force for the current call — the enclosing scope's, else global. */
81
+ /** The settings in force for the current call: the enclosing bridge's, else the process-wide ones. */
73
82
  function getConfig() {
74
83
  return scoped.getStore() ?? globalConfig;
75
84
  }
76
- /** The process-wide defaults, ignoring any active scope. */
85
+ /** The process-wide settings, ignoring any bridge scope. */
77
86
  function getGlobalConfig() {
78
87
  return globalConfig;
79
88
  }
@@ -81,7 +90,30 @@ function getGlobalConfig() {
81
90
  function withConfig(config, fn) {
82
91
  return scoped.run(config, fn);
83
92
  }
84
- /** Report one finished script run to the configured debug hook. */
93
+ /**
94
+ * How long each kind of run may take when no `timeoutMs` is set.
95
+ *
96
+ * Generous on purpose: a budget is a guard against a hung interpreter, not a
97
+ * performance target, and the first call against an Outlook that isn't running
98
+ * yet spends much of its budget starting it.
99
+ */
100
+ exports.RUN_BUDGETS = {
101
+ /** One lookup: accounts, signatures, opening or reading a single email. */
102
+ quick: 60_000,
103
+ /** Composing, filing, saving attachments, or reading one folder. */
104
+ standard: 120_000,
105
+ /** Walking a mailbox tree, or acting on a batch of items. */
106
+ scan: 300_000,
107
+ /** Emptying Deleted Items. */
108
+ purge: 600_000,
109
+ };
110
+ /** The timeout one run gets: the configured ceiling if there is one, else its budget. */
111
+ function timeoutFor(budget) {
112
+ if (budget === 'interactive')
113
+ return 0;
114
+ return getConfig().timeoutMs ?? exports.RUN_BUDGETS[budget];
115
+ }
116
+ /** Report one finished run to the configured debug hook. */
85
117
  function reportRun(event) {
86
118
  const { debug } = getConfig();
87
119
  if (!debug)
@@ -94,30 +126,53 @@ function reportRun(event) {
94
126
  process.stderr.write(`[outlook-bridge] ${event.runner} ${event.durationMs}ms ${outcome}\n${event.script}\n`);
95
127
  }
96
128
  /**
97
- * Path for a scratch file in the configured temp directory. The random suffix
98
- * matters: two concurrent calls landing in the same millisecond would otherwise
99
- * pick the same name and overwrite each other's script mid-run.
129
+ * A scratch-file path in the configured temp directory. The random part
130
+ * matters: two concurrent calls in the same millisecond would otherwise pick the
131
+ * same name and overwrite each other's script mid-run.
100
132
  */
101
133
  function tempFile(kind, extension) {
102
134
  const unique = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
103
135
  return path_1.default.join(getConfig().tempDir, `outlook-bridge-${kind}-${unique}.${extension}`);
104
136
  }
137
+ /** Delete a scratch file, ignoring one that is already gone. */
138
+ function removeTempFile(file) {
139
+ try {
140
+ fs_1.default.unlinkSync(file);
141
+ }
142
+ catch {
143
+ // The run is over either way; a leftover scratch file isn't worth failing it.
144
+ }
145
+ }
105
146
  /**
106
- * A fresh directory for one call's saved attachments.
147
+ * Where one call saves its attachments: the caller's directory (created if
148
+ * absent), or a fresh private directory of its own.
107
149
  *
108
- * Deliberately per-call rather than one shared folder: attachments are saved
109
- * under the name the sender gave them, so two emails that both carry
110
- * "invoice.pdf" would otherwise silently overwrite each other — and so would two
111
- * unrelated programs using this package on the same machine.
150
+ * Private by default because attachments keep the names senders gave them, so
151
+ * one shared folder means two emails carrying 'invoice.pdf' overwrite each
152
+ * other — and so do two programs using this package on one machine.
112
153
  */
113
- function makeAttachmentDir() {
114
- return fs_1.default.mkdtempSync(path_1.default.join(getConfig().tempDir, 'outlook-bridge-attachments-'));
154
+ function attachmentDestination(destDir) {
155
+ if (destDir) {
156
+ const existed = fs_1.default.existsSync(destDir);
157
+ fs_1.default.mkdirSync(destDir, { recursive: true });
158
+ return {
159
+ dir: destDir,
160
+ discardIfUnused() {
161
+ if (!existed)
162
+ removeIfEmpty(destDir);
163
+ },
164
+ };
165
+ }
166
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(getConfig().tempDir, 'outlook-bridge-attachments-'));
167
+ return { dir, discardIfUnused: () => removeIfEmpty(dir) };
115
168
  }
116
- /** Resolve a caller-supplied destination, creating it when absent. */
117
- function resolveDestDir(destDir) {
118
- if (!destDir)
119
- return makeAttachmentDir();
120
- fs_1.default.mkdirSync(destDir, { recursive: true });
121
- return destDir;
169
+ function removeIfEmpty(dir) {
170
+ try {
171
+ if (fs_1.default.readdirSync(dir).length === 0)
172
+ fs_1.default.rmdirSync(dir);
173
+ }
174
+ catch {
175
+ // Nothing to undo if it can't be read.
176
+ }
122
177
  }
123
178
  //# sourceMappingURL=runtime.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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","sourcesContent":["// Settings, and the scratch files every run needs.\n//\n// Each operation generates a script and runs it under an interpreter, which\n// leaves a caller four things they cannot otherwise reach: how long a run may\n// take, how much it may print, how to call it off, and what the script actually\n// said. Those are the settings here — process-wide through `configure()`, or per\n// bridge through `createOutlookBridge()`.\nimport { AsyncLocalStorage } from 'async_hooks';\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport { InvalidRequestError, type ScriptRunner } from './errors';\n\n/** One completed script run, handed to the `debug` hook. */\nexport interface BridgeDebugEvent {\n runner: ScriptRunner;\n /** The script exactly as it ran. */\n script: string;\n durationMs: number;\n /** Present only when the run failed. */\n error?: string;\n}\n\nexport interface BridgeOptions {\n /**\n * Ceiling on every script run, in milliseconds, replacing each operation's\n * own budget; 0 disables timeouts. Leave unset (or pass null to go back) to\n * let each operation use a budget sized for its work — a minute for a\n * lookup, up to ten for purging Deleted Items. `editEmailTemplate` waits on\n * a person and is never timed; cancel it with `signal`.\n */\n timeoutMs?: number | null;\n /**\n * Max bytes a script may print before the run is killed with\n * OUTPUT_TOO_LARGE. Searches that return whole bodies push against this.\n * Default 8 MiB.\n */\n maxBufferBytes?: number;\n /** Directory for generated scripts, and for saved attachments when a call\n * names no destination. Default: the OS temp directory. */\n tempDir?: string;\n /**\n * Cancels in-flight runs: the interpreter is killed and the call rejects\n * with ABORTED. Most useful scoped to the calls it should cancel:\n * `bridge.withOptions({ signal }).searchInboxByFilter(...)`.\n */\n signal?: AbortSignal;\n /** `true` logs every script to stderr; a function receives each run\n * instead. Default: on when OUTLOOK_BRIDGE_DEBUG is set to anything but '' or '0'. */\n debug?: boolean | ((event: BridgeDebugEvent) => void);\n}\n\n/** The settings a call runs with, every default applied. */\nexport interface ResolvedConfig {\n /** undefined = each operation's own budget. */\n readonly timeoutMs: number | undefined;\n readonly maxBufferBytes: number;\n readonly tempDir: string;\n readonly signal: AbortSignal | undefined;\n readonly debug: boolean | ((event: BridgeDebugEvent) => void);\n}\n\nconst envDebug = process.env.OUTLOOK_BRIDGE_DEBUG;\n\n/** The process-wide settings, as `configure()` leaves them. */\nlet globalConfig: ResolvedConfig = {\n timeoutMs: undefined,\n maxBufferBytes: 8 * 1024 * 1024,\n tempDir: os.tmpdir(),\n signal: undefined,\n debug: !!envDebug && envDebug !== '0',\n};\n\n/**\n * Per-bridge settings, scoped to an async call tree.\n *\n * The platform code reads its settings through `getConfig()` wherever it needs\n * them rather than having a config threaded through every function. A bridge\n * instance runs each call inside `withConfig`, and the store follows every\n * `await` beneath it — so two bridges with different timeouts can run at once\n * in one process without seeing each other's.\n */\nconst scoped = new AsyncLocalStorage<ResolvedConfig>();\n\n/** Caller options merged over a base; keys left out keep the base's value. */\nexport function mergeOptions(base: ResolvedConfig, options: BridgeOptions = {}): ResolvedConfig {\n const next = {...base};\n if (options.timeoutMs === null) next.timeoutMs = undefined;\n else if (options.timeoutMs !== undefined) next.timeoutMs = nonNegative(options.timeoutMs, 'timeoutMs');\n if (options.maxBufferBytes !== undefined) next.maxBufferBytes = Math.max(1, nonNegative(options.maxBufferBytes, 'maxBufferBytes'));\n if (options.tempDir !== undefined) next.tempDir = options.tempDir;\n if (options.signal !== undefined) next.signal = options.signal;\n if (options.debug !== undefined) next.debug = options.debug;\n return next;\n}\n\nfunction nonNegative(value: number, name: string): number {\n if (typeof value !== 'number' || Number.isNaN(value)) {\n throw new InvalidRequestError(`${name} must be a number (got ${String(value)}).`);\n }\n return Math.max(0, Math.floor(value));\n}\n\n/**\n * Change the process-wide settings. Merges, so naming one option leaves the\n * rest alone.\n *\n * Affects every caller in the process that uses the exported functions. Prefer\n * `createOutlookBridge(options)` in a process shared with code you don't own.\n */\nexport function configure(options: BridgeOptions): void {\n globalConfig = mergeOptions(globalConfig, options);\n}\n\n/** The settings in force for the current call: the enclosing bridge's, else the process-wide ones. */\nexport function getConfig(): ResolvedConfig {\n return scoped.getStore() ?? globalConfig;\n}\n\n/** The process-wide settings, ignoring any bridge scope. */\nexport function getGlobalConfig(): ResolvedConfig {\n return globalConfig;\n}\n\n/** Run `fn` with `config` in force for it and everything it awaits. */\nexport function withConfig<T>(config: ResolvedConfig, fn: () => T): T {\n return scoped.run(config, fn);\n}\n\n/**\n * How long each kind of run may take when no `timeoutMs` is set.\n *\n * Generous on purpose: a budget is a guard against a hung interpreter, not a\n * performance target, and the first call against an Outlook that isn't running\n * yet spends much of its budget starting it.\n */\nexport const RUN_BUDGETS = {\n /** One lookup: accounts, signatures, opening or reading a single email. */\n quick: 60_000,\n /** Composing, filing, saving attachments, or reading one folder. */\n standard: 120_000,\n /** Walking a mailbox tree, or acting on a batch of items. */\n scan: 300_000,\n /** Emptying Deleted Items. */\n purge: 600_000,\n} as const;\n\n/** A budget name, or 'interactive' for a run that waits on a person and is never timed. */\nexport type RunBudget = keyof typeof RUN_BUDGETS | 'interactive';\n\n/** The timeout one run gets: the configured ceiling if there is one, else its budget. */\nexport function timeoutFor(budget: RunBudget): number {\n if (budget === 'interactive') return 0;\n return getConfig().timeoutMs ?? RUN_BUDGETS[budget];\n}\n\n/** Report one finished run to the configured debug hook. */\nexport function reportRun(event: BridgeDebugEvent): void {\n const {debug} = getConfig();\n if (!debug) return;\n if (typeof debug === 'function') {\n debug(event);\n return;\n }\n const outcome = event.error ? `FAILED: ${event.error}` : 'ok';\n process.stderr.write(`[outlook-bridge] ${event.runner} ${event.durationMs}ms ${outcome}\\n${event.script}\\n`);\n}\n\n/**\n * A scratch-file path in the configured temp directory. The random part\n * matters: two concurrent calls in the same millisecond would otherwise pick the\n * same name and overwrite each other's script mid-run.\n */\nexport function tempFile(kind: string, extension: string): string {\n const unique = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;\n return path.join(getConfig().tempDir, `outlook-bridge-${kind}-${unique}.${extension}`);\n}\n\n/** Delete a scratch file, ignoring one that is already gone. */\nexport function removeTempFile(file: string): void {\n try {\n fs.unlinkSync(file);\n } catch {\n // The run is over either way; a leftover scratch file isn't worth failing it.\n }\n}\n\n/** A destination for saved attachments, and how to undo creating it. */\nexport interface AttachmentDestination {\n dir: string;\n\n /** Remove the directory if this call created it and left it empty. */\n discardIfUnused(): void;\n}\n\n/**\n * Where one call saves its attachments: the caller's directory (created if\n * absent), or a fresh private directory of its own.\n *\n * Private by default because attachments keep the names senders gave them, so\n * one shared folder means two emails carrying 'invoice.pdf' overwrite each\n * other — and so do two programs using this package on one machine.\n */\nexport function attachmentDestination(destDir?: string): AttachmentDestination {\n if (destDir) {\n const existed = fs.existsSync(destDir);\n fs.mkdirSync(destDir, {recursive: true});\n return {\n dir: destDir,\n discardIfUnused() {\n if (!existed) removeIfEmpty(destDir);\n },\n };\n }\n const dir = fs.mkdtempSync(path.join(getConfig().tempDir, 'outlook-bridge-attachments-'));\n return {dir, discardIfUnused: () => removeIfEmpty(dir)};\n}\n\nfunction removeIfEmpty(dir: string): void {\n try {\n if (fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);\n } catch {\n // Nothing to undo if it can't be read.\n }\n}\n"]}
@@ -0,0 +1,48 @@
1
+ import type { EmailLocator, EmailRef, Recipients } from '../types';
2
+ /** A required parameters object. */
3
+ export declare function requireObject<T extends object>(value: T | undefined | null, name: string): Partial<T>;
4
+ /** An optional options object; undefined and null are an empty one. */
5
+ export declare function optionsObject<T extends object>(value: T | undefined | null, name?: string): Partial<T>;
6
+ /** A required string, trimmed. Blank is an error. */
7
+ export declare function requiredText(value: unknown, name: string): string;
8
+ /** A required string kept exactly as given — content, not an identifier. May be empty. */
9
+ export declare function requiredContent(value: unknown, name: string): string;
10
+ /** Optional content kept exactly as given; undefined and null mean "not given". */
11
+ export declare function optionalContent(value: unknown, name: string): string | undefined;
12
+ /** An optional regular expression. */
13
+ export declare function optionalRegExp(value: unknown, name: string): RegExp | undefined;
14
+ /** An optional string, trimmed; undefined, null and blank all mean "not given". */
15
+ export declare function optionalText(value: unknown, name: string): string | undefined;
16
+ /** An optional flag; undefined and null take the fallback. */
17
+ export declare function flag(value: unknown, name: string, fallback: boolean): boolean;
18
+ /**
19
+ * An optional count: undefined and null take the fallback, a fraction is
20
+ * rounded down, and the result is clamped into [min, max] — the documented
21
+ * range is a guard, not a trap. Anything that isn't a finite number is an error.
22
+ */
23
+ export declare function count(value: unknown, name: string, fallback: number, min: number, max: number): number;
24
+ /** A list of strings, each non-blank and trimmed. Omitted is an empty list. */
25
+ export declare function textList(value: unknown, name: string): string[];
26
+ /** A list of names matched exactly as given — attachment file names, say. */
27
+ export declare function nameList(value: unknown, name: string): string[];
28
+ /** The entry ids a batch operation acts on. Duplicates are dropped; order is kept. */
29
+ export declare function entryIdList(value: unknown, name?: string): string[];
30
+ /** Where an email lives, from an id or anything carrying a locator. */
31
+ export declare function emailLocator(email: EmailRef | undefined | null, name?: string): EmailLocator;
32
+ /** A string→string map, e.g. template placeholders. */
33
+ export declare function stringRecord(value: unknown, name: string): Record<string, string> | undefined;
34
+ /**
35
+ * Split a recipient list into its entries.
36
+ *
37
+ * Separators are commas and semicolons — but not inside a quoted display name
38
+ * or between angle brackets, where `"Doe, Jo" <jo@example.com>` has a comma
39
+ * that belongs to the name.
40
+ */
41
+ export declare function recipientList(value: Recipients | undefined | null, name: string): string[];
42
+ /** One recipient entry as a display name and an address. */
43
+ export interface ParsedRecipient {
44
+ name: string;
45
+ address: string;
46
+ }
47
+ /** `"Doe, Jo" <jo@example.com>` → name + address; a bare address has no name. */
48
+ export declare function parseRecipient(entry: string): ParsedRecipient;
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requireObject = requireObject;
4
+ exports.optionsObject = optionsObject;
5
+ exports.requiredText = requiredText;
6
+ exports.requiredContent = requiredContent;
7
+ exports.optionalContent = optionalContent;
8
+ exports.optionalRegExp = optionalRegExp;
9
+ exports.optionalText = optionalText;
10
+ exports.flag = flag;
11
+ exports.count = count;
12
+ exports.textList = textList;
13
+ exports.nameList = nameList;
14
+ exports.entryIdList = entryIdList;
15
+ exports.emailLocator = emailLocator;
16
+ exports.stringRecord = stringRecord;
17
+ exports.recipientList = recipientList;
18
+ exports.parseRecipient = parseRecipient;
19
+ // Checking and normalizing what a caller passes, once, before either platform
20
+ // sees it.
21
+ //
22
+ // This is a security boundary as much as a convenience. Arguments end up
23
+ // inside generated scripts, and TypeScript's types vanish at run time: a
24
+ // JavaScript caller, or a tool layer passing JSON through, can hand a string
25
+ // where a count belongs. So every value is checked for its type here, every
26
+ // count is made a whole number within its documented range, and the platform
27
+ // code only ever receives values of the shape its script templates assume.
28
+ const errors_1 = require("../errors");
29
+ function describe(value) {
30
+ if (value === null)
31
+ return 'null';
32
+ if (Array.isArray(value))
33
+ return 'an array';
34
+ return typeof value === 'string' ? `'${value}'` : typeof value;
35
+ }
36
+ /** A required parameters object. */
37
+ function requireObject(value, name) {
38
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
39
+ throw new errors_1.InvalidRequestError(`${name} must be an object (got ${describe(value)}).`);
40
+ }
41
+ return value;
42
+ }
43
+ /** An optional options object; undefined and null are an empty one. */
44
+ function optionsObject(value, name = 'options') {
45
+ if (value === undefined || value === null)
46
+ return {};
47
+ return requireObject(value, name);
48
+ }
49
+ /** A required string, trimmed. Blank is an error. */
50
+ function requiredText(value, name) {
51
+ if (typeof value !== 'string' || value.trim() === '') {
52
+ throw new errors_1.InvalidRequestError(`${name} is required and must be a non-empty string (got ${describe(value)}).`);
53
+ }
54
+ return value.trim();
55
+ }
56
+ /** A required string kept exactly as given — content, not an identifier. May be empty. */
57
+ function requiredContent(value, name) {
58
+ if (typeof value !== 'string') {
59
+ throw new errors_1.InvalidRequestError(`${name} must be a string (got ${describe(value)}).`);
60
+ }
61
+ return value;
62
+ }
63
+ /** Optional content kept exactly as given; undefined and null mean "not given". */
64
+ function optionalContent(value, name) {
65
+ if (value === undefined || value === null)
66
+ return undefined;
67
+ return requiredContent(value, name);
68
+ }
69
+ /** An optional regular expression. */
70
+ function optionalRegExp(value, name) {
71
+ if (value === undefined || value === null)
72
+ return undefined;
73
+ // Checked by tag rather than instanceof, so a RegExp from another realm passes.
74
+ if (Object.prototype.toString.call(value) !== '[object RegExp]') {
75
+ throw new errors_1.InvalidRequestError(`${name} must be a RegExp (got ${describe(value)}).`);
76
+ }
77
+ return value;
78
+ }
79
+ /** An optional string, trimmed; undefined, null and blank all mean "not given". */
80
+ function optionalText(value, name) {
81
+ if (value === undefined || value === null)
82
+ return undefined;
83
+ if (typeof value !== 'string') {
84
+ throw new errors_1.InvalidRequestError(`${name} must be a string (got ${describe(value)}).`);
85
+ }
86
+ return value.trim() || undefined;
87
+ }
88
+ /** An optional flag; undefined and null take the fallback. */
89
+ function flag(value, name, fallback) {
90
+ if (value === undefined || value === null)
91
+ return fallback;
92
+ if (typeof value !== 'boolean') {
93
+ throw new errors_1.InvalidRequestError(`${name} must be true or false (got ${describe(value)}).`);
94
+ }
95
+ return value;
96
+ }
97
+ /**
98
+ * An optional count: undefined and null take the fallback, a fraction is
99
+ * rounded down, and the result is clamped into [min, max] — the documented
100
+ * range is a guard, not a trap. Anything that isn't a finite number is an error.
101
+ */
102
+ function count(value, name, fallback, min, max) {
103
+ if (value === undefined || value === null)
104
+ return fallback;
105
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
106
+ throw new errors_1.InvalidRequestError(`${name} must be a finite number (got ${describe(value)}).`);
107
+ }
108
+ return Math.min(max, Math.max(min, Math.floor(value)));
109
+ }
110
+ /** A list of strings, each non-blank and trimmed. Omitted is an empty list. */
111
+ function textList(value, name) {
112
+ if (value === undefined || value === null)
113
+ return [];
114
+ if (!Array.isArray(value)) {
115
+ throw new errors_1.InvalidRequestError(`${name} must be an array of strings (got ${describe(value)}).`);
116
+ }
117
+ return value.map((item, index) => requiredText(item, `${name}[${index}]`));
118
+ }
119
+ /** A list of names matched exactly as given — attachment file names, say. */
120
+ function nameList(value, name) {
121
+ if (!Array.isArray(value)) {
122
+ throw new errors_1.InvalidRequestError(`${name} must be an array of strings (got ${describe(value)}).`);
123
+ }
124
+ return value.map((item, index) => {
125
+ if (typeof item !== 'string' || item.trim() === '') {
126
+ throw new errors_1.InvalidRequestError(`${name}[${index}] must be a non-empty string (got ${describe(item)}).`);
127
+ }
128
+ return item;
129
+ });
130
+ }
131
+ /** The entry ids a batch operation acts on. Duplicates are dropped; order is kept. */
132
+ function entryIdList(value, name = 'entryIds') {
133
+ if (!Array.isArray(value)) {
134
+ throw new errors_1.InvalidRequestError(`${name} must be an array of entry ids (got ${describe(value)}).`);
135
+ }
136
+ return [...new Set(value.map((id, index) => requiredText(id, `${name}[${index}]`)))];
137
+ }
138
+ /** Where an email lives, from an id or anything carrying a locator. */
139
+ function emailLocator(email, name = 'email') {
140
+ if (typeof email === 'string')
141
+ return { entryId: requiredText(email, name) };
142
+ if (email && typeof email === 'object') {
143
+ const entryId = requiredText(email.entryId, `${name}.entryId`);
144
+ const storeId = optionalText(email.storeId, `${name}.storeId`);
145
+ return storeId ? { entryId, storeId } : { entryId };
146
+ }
147
+ throw new errors_1.InvalidRequestError(`${name} must be an entry id or an object with an entryId (got ${describe(email)}).`);
148
+ }
149
+ /** A string→string map, e.g. template placeholders. */
150
+ function stringRecord(value, name) {
151
+ if (value === undefined || value === null)
152
+ return undefined;
153
+ if (typeof value !== 'object' || Array.isArray(value)) {
154
+ throw new errors_1.InvalidRequestError(`${name} must be an object of strings (got ${describe(value)}).`);
155
+ }
156
+ const out = {};
157
+ for (const [key, entry] of Object.entries(value)) {
158
+ if (typeof entry !== 'string') {
159
+ throw new errors_1.InvalidRequestError(`${name}.${key} must be a string (got ${describe(entry)}).`);
160
+ }
161
+ out[key] = entry;
162
+ }
163
+ return out;
164
+ }
165
+ /**
166
+ * Split a recipient list into its entries.
167
+ *
168
+ * Separators are commas and semicolons — but not inside a quoted display name
169
+ * or between angle brackets, where `"Doe, Jo" <jo@example.com>` has a comma
170
+ * that belongs to the name.
171
+ */
172
+ function recipientList(value, name) {
173
+ if (value === undefined || value === null)
174
+ return [];
175
+ const parts = Array.isArray(value) ? value : [value];
176
+ const out = [];
177
+ parts.forEach((part, index) => {
178
+ if (typeof part !== 'string') {
179
+ throw new errors_1.InvalidRequestError(`${name}[${index}] must be a string (got ${describe(part)}).`);
180
+ }
181
+ out.push(...splitAddressList(part));
182
+ });
183
+ return out;
184
+ }
185
+ function splitAddressList(text) {
186
+ const entries = [];
187
+ let current = '';
188
+ let quoted = false;
189
+ let angled = false;
190
+ for (const ch of text) {
191
+ if (ch === '"' && !angled)
192
+ quoted = !quoted;
193
+ else if (ch === '<' && !quoted)
194
+ angled = true;
195
+ else if (ch === '>' && !quoted)
196
+ angled = false;
197
+ if ((ch === ',' || ch === ';') && !quoted && !angled) {
198
+ entries.push(current);
199
+ current = '';
200
+ }
201
+ else {
202
+ current += ch;
203
+ }
204
+ }
205
+ entries.push(current);
206
+ return entries.map(entry => entry.trim()).filter(Boolean);
207
+ }
208
+ /** `"Doe, Jo" <jo@example.com>` → name + address; a bare address has no name. */
209
+ function parseRecipient(entry) {
210
+ const angled = /^(.*)<([^<>]+)>\s*$/.exec(entry);
211
+ if (!angled)
212
+ return { name: '', address: entry.trim() };
213
+ const name = angled[1].trim().replace(/^"(.*)"$/, '$1').trim();
214
+ return { name, address: angled[2].trim() };
215
+ }
216
+ //# sourceMappingURL=args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/shared/args.ts"],"names":[],"mappings":"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","sourcesContent":["// Checking and normalizing what a caller passes, once, before either platform\n// sees it.\n//\n// This is a security boundary as much as a convenience. Arguments end up\n// inside generated scripts, and TypeScript's types vanish at run time: a\n// JavaScript caller, or a tool layer passing JSON through, can hand a string\n// where a count belongs. So every value is checked for its type here, every\n// count is made a whole number within its documented range, and the platform\n// code only ever receives values of the shape its script templates assume.\nimport { InvalidRequestError } from '../errors';\nimport type { EmailLocator, EmailRef, Recipients } from '../types';\n\nfunction describe(value: unknown): string {\n if (value === null) return 'null';\n if (Array.isArray(value)) return 'an array';\n return typeof value === 'string' ? `'${value}'` : typeof value;\n}\n\n/** A required parameters object. */\nexport function requireObject<T extends object>(value: T | undefined | null, name: string): Partial<T> {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n throw new InvalidRequestError(`${name} must be an object (got ${describe(value)}).`);\n }\n return value;\n}\n\n/** An optional options object; undefined and null are an empty one. */\nexport function optionsObject<T extends object>(value: T | undefined | null, name = 'options'): Partial<T> {\n if (value === undefined || value === null) return {};\n return requireObject(value, name);\n}\n\n/** A required string, trimmed. Blank is an error. */\nexport function requiredText(value: unknown, name: string): string {\n if (typeof value !== 'string' || value.trim() === '') {\n throw new InvalidRequestError(`${name} is required and must be a non-empty string (got ${describe(value)}).`);\n }\n return value.trim();\n}\n\n/** A required string kept exactly as given — content, not an identifier. May be empty. */\nexport function requiredContent(value: unknown, name: string): string {\n if (typeof value !== 'string') {\n throw new InvalidRequestError(`${name} must be a string (got ${describe(value)}).`);\n }\n return value;\n}\n\n/** Optional content kept exactly as given; undefined and null mean \"not given\". */\nexport function optionalContent(value: unknown, name: string): string | undefined {\n if (value === undefined || value === null) return undefined;\n return requiredContent(value, name);\n}\n\n/** An optional regular expression. */\nexport function optionalRegExp(value: unknown, name: string): RegExp | undefined {\n if (value === undefined || value === null) return undefined;\n // Checked by tag rather than instanceof, so a RegExp from another realm passes.\n if (Object.prototype.toString.call(value) !== '[object RegExp]') {\n throw new InvalidRequestError(`${name} must be a RegExp (got ${describe(value)}).`);\n }\n return value as RegExp;\n}\n\n/** An optional string, trimmed; undefined, null and blank all mean \"not given\". */\nexport function optionalText(value: unknown, name: string): string | undefined {\n if (value === undefined || value === null) return undefined;\n if (typeof value !== 'string') {\n throw new InvalidRequestError(`${name} must be a string (got ${describe(value)}).`);\n }\n return value.trim() || undefined;\n}\n\n/** An optional flag; undefined and null take the fallback. */\nexport function flag(value: unknown, name: string, fallback: boolean): boolean {\n if (value === undefined || value === null) return fallback;\n if (typeof value !== 'boolean') {\n throw new InvalidRequestError(`${name} must be true or false (got ${describe(value)}).`);\n }\n return value;\n}\n\n/**\n * An optional count: undefined and null take the fallback, a fraction is\n * rounded down, and the result is clamped into [min, max] — the documented\n * range is a guard, not a trap. Anything that isn't a finite number is an error.\n */\nexport function count(value: unknown, name: string, fallback: number, min: number, max: number): number {\n if (value === undefined || value === null) return fallback;\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n throw new InvalidRequestError(`${name} must be a finite number (got ${describe(value)}).`);\n }\n return Math.min(max, Math.max(min, Math.floor(value)));\n}\n\n/** A list of strings, each non-blank and trimmed. Omitted is an empty list. */\nexport function textList(value: unknown, name: string): string[] {\n if (value === undefined || value === null) return [];\n if (!Array.isArray(value)) {\n throw new InvalidRequestError(`${name} must be an array of strings (got ${describe(value)}).`);\n }\n return value.map((item, index) => requiredText(item, `${name}[${index}]`));\n}\n\n/** A list of names matched exactly as given — attachment file names, say. */\nexport function nameList(value: unknown, name: string): string[] {\n if (!Array.isArray(value)) {\n throw new InvalidRequestError(`${name} must be an array of strings (got ${describe(value)}).`);\n }\n return value.map((item, index) => {\n if (typeof item !== 'string' || item.trim() === '') {\n throw new InvalidRequestError(`${name}[${index}] must be a non-empty string (got ${describe(item)}).`);\n }\n return item;\n });\n}\n\n/** The entry ids a batch operation acts on. Duplicates are dropped; order is kept. */\nexport function entryIdList(value: unknown, name = 'entryIds'): string[] {\n if (!Array.isArray(value)) {\n throw new InvalidRequestError(`${name} must be an array of entry ids (got ${describe(value)}).`);\n }\n return [...new Set(value.map((id, index) => requiredText(id, `${name}[${index}]`)))];\n}\n\n/** Where an email lives, from an id or anything carrying a locator. */\nexport function emailLocator(email: EmailRef | undefined | null, name = 'email'): EmailLocator {\n if (typeof email === 'string') return {entryId: requiredText(email, name)};\n if (email && typeof email === 'object') {\n const entryId = requiredText(email.entryId, `${name}.entryId`);\n const storeId = optionalText(email.storeId, `${name}.storeId`);\n return storeId ? {entryId, storeId} : {entryId};\n }\n throw new InvalidRequestError(`${name} must be an entry id or an object with an entryId (got ${describe(email)}).`);\n}\n\n/** A string→string map, e.g. template placeholders. */\nexport function stringRecord(value: unknown, name: string): Record<string, string> | undefined {\n if (value === undefined || value === null) return undefined;\n if (typeof value !== 'object' || Array.isArray(value)) {\n throw new InvalidRequestError(`${name} must be an object of strings (got ${describe(value)}).`);\n }\n const out: Record<string, string> = {};\n for (const [key, entry] of Object.entries(value)) {\n if (typeof entry !== 'string') {\n throw new InvalidRequestError(`${name}.${key} must be a string (got ${describe(entry)}).`);\n }\n out[key] = entry;\n }\n return out;\n}\n\n/**\n * Split a recipient list into its entries.\n *\n * Separators are commas and semicolons — but not inside a quoted display name\n * or between angle brackets, where `\"Doe, Jo\" <jo@example.com>` has a comma\n * that belongs to the name.\n */\nexport function recipientList(value: Recipients | undefined | null, name: string): string[] {\n if (value === undefined || value === null) return [];\n const parts = Array.isArray(value) ? value : [value];\n const out: string[] = [];\n parts.forEach((part, index) => {\n if (typeof part !== 'string') {\n throw new InvalidRequestError(`${name}[${index}] must be a string (got ${describe(part)}).`);\n }\n out.push(...splitAddressList(part));\n });\n return out;\n}\n\nfunction splitAddressList(text: string): string[] {\n const entries: string[] = [];\n let current = '';\n let quoted = false;\n let angled = false;\n for (const ch of text) {\n if (ch === '\"' && !angled) quoted = !quoted;\n else if (ch === '<' && !quoted) angled = true;\n else if (ch === '>' && !quoted) angled = false;\n if ((ch === ',' || ch === ';') && !quoted && !angled) {\n entries.push(current);\n current = '';\n } else {\n current += ch;\n }\n }\n entries.push(current);\n return entries.map(entry => entry.trim()).filter(Boolean);\n}\n\n/** One recipient entry as a display name and an address. */\nexport interface ParsedRecipient {\n name: string;\n address: string;\n}\n\n/** `\"Doe, Jo\" <jo@example.com>` → name + address; a bare address has no name. */\nexport function parseRecipient(entry: string): ParsedRecipient {\n const angled = /^(.*)<([^<>]+)>\\s*$/.exec(entry);\n if (!angled) return {name: '', address: entry.trim()};\n const name = angled[1].trim().replace(/^\"(.*)\"$/, '$1').trim();\n return {name, address: angled[2].trim()};\n}\n"]}
@@ -1,32 +1,44 @@
1
1
  /**
2
2
  * Collapse whitespace runs (non-breaking spaces included) and trim, so a
3
- * trivially reformatted filename still resolves. Both platforms' regex engines
4
- * treat `\s` as covering U+00A0, which is the character a mail client is most
5
- * likely to have substituted.
3
+ * trivially reformatted file name still resolves. Both platforms' regex engines
4
+ * count U+00A0 as whitespace.
6
5
  */
7
6
  export declare function normalizeAttachmentName(name: string): string;
8
7
  /**
9
- * Index of the attachment matching `wanted`, or -1.
10
- *
11
- * Exact first, then whitespace-normalized and case-insensitive — in that order,
12
- * so an email carrying two names differing only in case still resolves the one
13
- * that was actually asked for.
8
+ * Index of the attachment matching `wanted`, or -1: exact first, then
9
+ * whitespace-normalized and case-insensitive — in that order, so two names that
10
+ * differ only in case still resolve the one asked for.
14
11
  */
15
12
  export declare function findAttachmentIndex(names: readonly string[], wanted: string): number;
16
13
  /**
17
- * The "attachment not found" message, worded once for both platforms.
18
- *
19
- * Every field arrives already stringified, which is what lets the Windows script
20
- * build the identical sentence: it passes PowerShell interpolations
21
- * (`$($item.Subject)`) where macOS passes the values it read.
14
+ * The "attachment not found" message, worded once for both platforms. Fields
15
+ * arrive as strings so the Windows script can build the same sentence from
16
+ * PowerShell variables where macOS passes the values it read.
22
17
  */
23
18
  export declare function attachmentNotFoundMessage(init: {
24
19
  fileName: string;
25
20
  senderEmail: string;
26
21
  subject: string;
27
22
  receivedTime: string;
28
- /** The attachments the email does carry, already joined for display. */
23
+ /** The attachments the email does carry, joined for display. */
29
24
  present: string;
30
- /** What this platform calls the id, for the closing advice. */
31
- idLabel: string;
32
25
  }): string;
26
+ /** Characters no file name may carry on Windows (a superset of macOS's). */
27
+ export declare const UNSAFE_FILE_NAME_CHARS = "[<>:\"/\\\\|?*\\x00-\\x1F]";
28
+ /** Names Windows reserves for devices, with or without an extension. */
29
+ export declare const RESERVED_FILE_NAME = "^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\\..*)?$";
30
+ /** How long a saved name's stem may run before it is cut. */
31
+ export declare const MAX_FILE_STEM = 150;
32
+ /**
33
+ * A sender-chosen attachment name made safe to write: no path separators or
34
+ * characters a file system rejects, no trailing dots or spaces, no reserved
35
+ * device name, and a stem short enough to stay clear of path-length limits. A
36
+ * name like `..\..\x.exe` therefore lands inside the destination as
37
+ * `.._.._x.exe`, never outside it.
38
+ */
39
+ export declare function safeFileName(name: string): string;
40
+ /**
41
+ * A path in `dir` for `name` that `exists` reports free: the name itself, else
42
+ * `name (1).ext`, `name (2).ext`, … Existing files are never overwritten.
43
+ */
44
+ export declare function uniqueSavePath(dir: string, name: string, exists: (file: string) => boolean): string;