@dawnlit/outlook-bridge 2.0.0 → 2.1.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 +6 -4
- package/dist/backend.d.ts +2 -2
- package/dist/backend.js.map +1 -1
- package/dist/bridge.js +3 -3
- package/dist/bridge.js.map +1 -1
- package/dist/errors.d.ts +0 -2
- package/dist/errors.js +1 -6
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/mac/accounts.js.map +1 -1
- package/dist/mac/attachments.js +1 -1
- package/dist/mac/attachments.js.map +1 -1
- package/dist/mac/bounces.js +8 -12
- package/dist/mac/bounces.js.map +1 -1
- package/dist/mac/cleanup.js +4 -41
- package/dist/mac/cleanup.js.map +1 -1
- package/dist/mac/drafts.d.ts +3 -1
- package/dist/mac/drafts.js +34 -33
- package/dist/mac/drafts.js.map +1 -1
- package/dist/mac/folders.d.ts +5 -3
- package/dist/mac/folders.js +18 -19
- package/dist/mac/folders.js.map +1 -1
- package/dist/mac/profile.js.map +1 -1
- package/dist/mac/read.d.ts +4 -0
- package/dist/mac/read.js +54 -16
- package/dist/mac/read.js.map +1 -1
- package/dist/mac/run.d.ts +19 -2
- package/dist/mac/run.js +42 -2
- package/dist/mac/run.js.map +1 -1
- package/dist/mac/scripts.d.ts +21 -14
- package/dist/mac/scripts.js +52 -21
- package/dist/mac/scripts.js.map +1 -1
- package/dist/mac/send.d.ts +3 -3
- package/dist/mac/send.js +44 -12
- package/dist/mac/send.js.map +1 -1
- package/dist/mac/templates.js +15 -18
- package/dist/mac/templates.js.map +1 -1
- package/dist/mail.d.ts +5 -3
- package/dist/mail.js +19 -6
- package/dist/mail.js.map +1 -1
- package/dist/runtime.js.map +1 -1
- package/dist/shared/args.js.map +1 -1
- package/dist/shared/attachmentMatch.d.ts +0 -6
- package/dist/shared/attachmentMatch.js +0 -1
- package/dist/shared/attachmentMatch.js.map +1 -1
- package/dist/shared/exec.js.map +1 -1
- package/dist/shared/json.d.ts +11 -6
- package/dist/shared/json.js +12 -7
- package/dist/shared/json.js.map +1 -1
- package/dist/shared/locate.d.ts +0 -8
- package/dist/shared/locate.js +0 -1
- package/dist/shared/locate.js.map +1 -1
- package/dist/shared/replyBody.d.ts +0 -15
- package/dist/shared/replyBody.js +0 -2
- package/dist/shared/replyBody.js.map +1 -1
- package/dist/shared/templateLimits.d.ts +14 -0
- package/dist/shared/templateLimits.js +21 -0
- package/dist/shared/templateLimits.js.map +1 -0
- package/dist/templateBody.d.ts +6 -0
- package/dist/templateBody.js +8 -1
- package/dist/templateBody.js.map +1 -1
- package/dist/types.d.ts +23 -3
- package/dist/types.js.map +1 -1
- package/dist/windows/bounces.js +4 -4
- package/dist/windows/bounces.js.map +1 -1
- package/dist/windows/cleanup.d.ts +5 -1
- package/dist/windows/cleanup.js +16 -11
- package/dist/windows/cleanup.js.map +1 -1
- package/dist/windows/drafts.d.ts +4 -4
- package/dist/windows/drafts.js +20 -31
- package/dist/windows/drafts.js.map +1 -1
- package/dist/windows/folders.js +1 -1
- package/dist/windows/folders.js.map +1 -1
- package/dist/windows/read.js +6 -2
- package/dist/windows/read.js.map +1 -1
- package/dist/windows/run.d.ts +0 -2
- package/dist/windows/run.js +1 -2
- package/dist/windows/run.js.map +1 -1
- package/dist/windows/scripts.d.ts +4 -4
- package/dist/windows/scripts.js +10 -10
- package/dist/windows/scripts.js.map +1 -1
- package/dist/windows/send.d.ts +8 -3
- package/dist/windows/send.js +10 -2
- package/dist/windows/send.js.map +1 -1
- package/dist/windows/templates.js +9 -15
- package/dist/windows/templates.js.map +1 -1
- package/package.json +2 -2
package/dist/windows/run.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/windows/run.ts"],"names":[],"mappings":"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","sourcesContent":["// Getting a generated script to Windows PowerShell and its output back.\n//\n// Every Windows operation is \"build a script, run it, read what it printed\",\n// so this module owns the parts nobody should reimplement: how a script is\n// wrapped and run, how a failure is read back out of it, and the escaping rules\n// for putting caller text into a script safely.\nimport fs from 'fs';\nimport { InvalidRequestError, ScriptError, UnsupportedPlatformError } from '../errors';\nimport { removeTempFile, type RunBudget, tempFile } from '../runtime';\nimport { runScriptProcess } from '../shared/exec';\n\nconst RUNNER = 'powershell' as const;\n\n/** Opens the line a script's catch block writes its failure on. */\nconst FAILURE_MARKER = '@@outlook-bridge-failure@@';\n\n/** Refuse before generating anything when this isn't Windows. */\nexport function requireWindows(): void {\n if (process.platform !== 'win32') throw new UnsupportedPlatformError(process.platform);\n}\n\n// ── Escaping ─────────────────────────────────────────────────────────────\n//\n// Caller text goes into a script ONLY as a single-quoted literal, built by\n// `psString`. Two things make that the rule:\n// - A double-quoted PowerShell string expands `$(...)`, so caller text placed\n// in one runs as code. Where a message needs caller text, it is assigned to a\n// variable first and the double-quoted string names the variable — a\n// variable's value is never evaluated again.\n// - PowerShell accepts the typographic quotes ‘ ’ ‚ ‛ as single quotes too, so\n// doubling only the ASCII one leaves a way out of the literal. Every quote\n// character is doubled, which PowerShell reads back as that same character.\n\n/** Escape text for the inside of a PowerShell single-quoted literal. */\nexport function psEscape(text: string): string {\n return text.replace(/['\\u2018\\u2019\\u201A\\u201B]/g, quote => quote + quote);\n}\n\n/** Caller text as a PowerShell single-quoted literal. */\nexport function psString(text: string): string {\n return `'${psEscape(text)}'`;\n}\n\n/** Strings as a PowerShell array expression: `@('a', 'b')`. */\nexport function psArray(values: readonly string[]): string {\n return `@(${values.map(psString).join(', ')})`;\n}\n\n/** `$true` / `$false` — PowerShell has no bare boolean literal. */\nexport function psBool(value: boolean): string {\n return value ? '$true' : '$false';\n}\n\n/**\n * A whole number for a script. The operations layer has already validated every\n * count, so this refusing anything else is the second line of defence: a\n * non-number never reaches a script as code.\n */\nexport function psInt(value: number): string {\n if (!Number.isSafeInteger(value)) {\n throw new InvalidRequestError(`Expected a whole number, got ${String(value)}.`);\n }\n return String(value);\n}\n\n// ── Running ──────────────────────────────────────────────────────────────\n\n/**\n * The complete script for a body: UTF-8 output, stop-on-error, and a catch that\n * reports any failure as one JSON line on stderr.\n *\n * UTF-8 output matters because Windows PowerShell 5.1 otherwise writes in the\n * console's legacy code page, whose \"best fit\" mapping rewrites non-ASCII — a\n * curly quote in a subject becomes a straight one — which corrupts the JSON\n * the scripts print. The catch matters because an uncaught PowerShell error\n * prints a multi-line error record (\"At line:12 char:48 …\") that would\n * otherwise become the error's message.\n */\nexport function buildScript(body: string): string {\n return `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\n$ErrorActionPreference = 'Stop'\n$WarningPreference = 'SilentlyContinue'\n$ProgressPreference = 'SilentlyContinue'\ntry {\n${body}\n} catch {\n $failure = $_\n $message = [string]$failure.Exception.Message\n if (-not $message) { $message = [string]$failure }\n $line = 0\n try { $line = [int]$failure.InvocationInfo.ScriptLineNumber } catch {}\n [Console]::Error.WriteLine('${FAILURE_MARKER}' + (ConvertTo-Json -Compress @{ message = $message; line = $line }))\n exit 1\n}\n`;\n}\n\n/** The failure a script's catch block reported, or its raw stderr when it never got that far. */\nfunction parseFailure(stderr: string): { message: string; line?: number } {\n const at = stderr.lastIndexOf(FAILURE_MARKER);\n if (at !== -1) {\n const json = stderr.slice(at + FAILURE_MARKER.length).split(/\\r?\\n/)[0];\n try {\n const parsed = JSON.parse(json) as { message?: unknown; line?: unknown };\n const line = typeof parsed.line === 'number' && parsed.line > 0 ? parsed.line : undefined;\n return {message: String(parsed.message ?? ''), line};\n } catch {\n // Fall through to the raw stderr.\n }\n }\n // A script that fails to PARSE never enters its try block, so PowerShell's\n // own report is all there is.\n return {message: stderr.trim()};\n}\n\nasync function execute(body: string, budget: RunBudget): Promise<{\n stdout: string;\n script: string;\n durationMs: number\n}> {\n requireWindows();\n const script = buildScript(body);\n // Always from a file, never `-Command`: a file has no command-line length\n // limit and no second layer of quoting. The byte-order mark is what makes\n // PowerShell 5.1 read it as UTF-8 rather than the ANSI code page, which\n // would mangle every non-ASCII character in a subject, name or path.\n const file = tempFile('script', 'ps1');\n fs.writeFileSync(file, '\\uFEFF' + script, 'utf8');\n try {\n const {stdout, durationMs} = await runScriptProcess({\n runner: RUNNER,\n command: 'powershell.exe',\n args: ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', file],\n script,\n budget,\n parseFailure,\n });\n return {stdout, script, durationMs};\n } finally {\n removeTempFile(file);\n }\n}\n\n/** Run a script body and return what it printed, trimmed. */\nexport async function runPowerShell(body: string, budget: RunBudget): Promise<string> {\n return (await execute(body, budget)).stdout.trim();\n}\n\n/**\n * Run a script body that prints JSON, and return it parsed — null when the\n * script printed nothing. Output that isn't JSON is a SCRIPT_FAILED carrying\n * the script, never a bare SyntaxError.\n */\nexport async function runPowerShellJson(body: string, budget: RunBudget): Promise<unknown> {\n const {stdout, script, durationMs} = await execute(body, budget);\n const output = stdout.trim();\n if (output === '' || output === 'null') return null;\n try {\n return JSON.parse(output);\n } catch (error) {\n throw new ScriptError({\n runner: RUNNER,\n script,\n stderr: '',\n durationMs,\n message: `PowerShell printed output that is not valid JSON: ${output.slice(0, 200)}`,\n cause: error,\n });\n }\n}\n\n/**\n * Write text a script will read back, and hand back its cleanup.\n *\n * Large HTML — an email body, a Word-sized template — goes through a file rather\n * than the script itself, where it would have to be escaped and would bloat\n * every error and debug event that carries the script.\n */\nexport function scriptInput(kind: string, contents: string): { path: string; cleanup(): void } {\n const file = tempFile(kind, 'html');\n fs.writeFileSync(file, contents, 'utf8');\n return {path: file, cleanup: () => removeTempFile(file)};\n}\n\n/**\n * Read a file a script wrote, as UTF-8, and delete it; '' when it never\n * appeared. .NET's UTF-8 writer opens the file with a byte-order mark, which is\n * not part of the text.\n */\nexport function readScriptOutput(file: string): string {\n try {\n return fs.readFileSync(file, 'utf8').replace(/^\\uFEFF/, '');\n } catch {\n return '';\n } finally {\n removeTempFile(file);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/windows/run.ts"],"names":[],"mappings":"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","sourcesContent":["// Getting a generated script to Windows PowerShell and its output back.\n//\n// Every Windows operation is \"build a script, run it, read what it printed\",\n// so this module owns the parts nobody should reimplement: how a script is\n// wrapped and run, how a failure is read back out of it, and the escaping rules\n// for putting caller text into a script safely.\nimport fs from 'fs';\nimport { InvalidRequestError, ScriptError, UnsupportedPlatformError } from '../errors';\nimport { removeTempFile, type RunBudget, tempFile } from '../runtime';\nimport { type ProcessOutput, runScriptProcess } from '../shared/exec';\n\nconst RUNNER = 'powershell' as const;\n\n/** Opens the line a script's catch block writes its failure on. */\nconst FAILURE_MARKER = '@@outlook-bridge-failure@@';\n\n/** Refuse before writing or running a script when this isn't Windows. */\nfunction requireWindows(): void {\n if (process.platform !== 'win32') throw new UnsupportedPlatformError(process.platform);\n}\n\n// ── Escaping ─────────────────────────────────────────────────────────────\n//\n// Caller text goes into a script ONLY as a single-quoted literal, built by\n// `psString`. Two things make that the rule:\n// - A double-quoted PowerShell string expands `$(...)`, so caller text placed\n// in one runs as code. Where a message needs caller text, it is assigned to a\n// variable first and the double-quoted string names the variable — a\n// variable's value is never evaluated again.\n// - PowerShell accepts the typographic quotes ‘ ’ ‚ ‛ as single quotes too, so\n// doubling only the ASCII one leaves a way out of the literal. Every quote\n// character is doubled, which PowerShell reads back as that same character.\n\n/** Escape text for the inside of a PowerShell single-quoted literal. */\nexport function psEscape(text: string): string {\n return text.replace(/['\\u2018\\u2019\\u201A\\u201B]/g, quote => quote + quote);\n}\n\n/** Caller text as a PowerShell single-quoted literal. */\nexport function psString(text: string): string {\n return `'${psEscape(text)}'`;\n}\n\n/** Strings as a PowerShell array expression: `@('a', 'b')`. */\nexport function psArray(values: readonly string[]): string {\n return `@(${values.map(psString).join(', ')})`;\n}\n\n/** `$true` / `$false` — PowerShell has no bare boolean literal. */\nexport function psBool(value: boolean): string {\n return value ? '$true' : '$false';\n}\n\n/**\n * A whole number for a script. The operations layer has already validated every\n * count, so this refusing anything else is the second line of defence: a\n * non-number never reaches a script as code.\n */\nexport function psInt(value: number): string {\n if (!Number.isSafeInteger(value)) {\n throw new InvalidRequestError(`Expected a whole number, got ${String(value)}.`);\n }\n return String(value);\n}\n\n// ── Running ──────────────────────────────────────────────────────────────\n\n/**\n * The complete script for a body: UTF-8 output, stop-on-error, and a catch that\n * reports any failure as one JSON line on stderr.\n *\n * UTF-8 output matters because Windows PowerShell 5.1 otherwise writes in the\n * console's legacy code page, whose \"best fit\" mapping rewrites non-ASCII — a\n * curly quote in a subject becomes a straight one — which corrupts the JSON\n * the scripts print. The catch matters because an uncaught PowerShell error\n * prints a multi-line error record (\"At line:12 char:48 …\") that would\n * otherwise become the error's message.\n */\nexport function buildScript(body: string): string {\n return `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\n$ErrorActionPreference = 'Stop'\n$WarningPreference = 'SilentlyContinue'\n$ProgressPreference = 'SilentlyContinue'\ntry {\n${body}\n} catch {\n $failure = $_\n $message = [string]$failure.Exception.Message\n if (-not $message) { $message = [string]$failure }\n $line = 0\n try { $line = [int]$failure.InvocationInfo.ScriptLineNumber } catch {}\n [Console]::Error.WriteLine('${FAILURE_MARKER}' + (ConvertTo-Json -Compress @{ message = $message; line = $line }))\n exit 1\n}\n`;\n}\n\n/** The failure a script's catch block reported, or its raw stderr when it never got that far. */\nfunction parseFailure(stderr: string): { message: string; line?: number } {\n const at = stderr.lastIndexOf(FAILURE_MARKER);\n if (at !== -1) {\n const json = stderr.slice(at + FAILURE_MARKER.length).split(/\\r?\\n/)[0];\n try {\n const parsed = JSON.parse(json) as { message?: unknown; line?: unknown };\n const line = typeof parsed.line === 'number' && parsed.line > 0 ? parsed.line : undefined;\n return { message: String(parsed.message ?? ''), line };\n } catch {\n // Fall through to the raw stderr.\n }\n }\n // A script that fails to PARSE never enters its try block, so PowerShell's\n // own report is all there is.\n return { message: stderr.trim() };\n}\n\nasync function execute(body: string, budget: RunBudget): Promise<ProcessOutput & { script: string }> {\n requireWindows();\n const script = buildScript(body);\n // Always from a file, never `-Command`: a file has no command-line length\n // limit and no second layer of quoting. The byte-order mark is what makes\n // PowerShell 5.1 read it as UTF-8 rather than the ANSI code page, which\n // would mangle every non-ASCII character in a subject, name or path.\n const file = tempFile('script', 'ps1');\n fs.writeFileSync(file, '\\uFEFF' + script, 'utf8');\n try {\n const { stdout, durationMs } = await runScriptProcess({\n runner: RUNNER,\n command: 'powershell.exe',\n args: ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', file],\n script,\n budget,\n parseFailure,\n });\n return { stdout, script, durationMs };\n } finally {\n removeTempFile(file);\n }\n}\n\n/** Run a script body and return what it printed, trimmed. */\nexport async function runPowerShell(body: string, budget: RunBudget): Promise<string> {\n return (await execute(body, budget)).stdout.trim();\n}\n\n/**\n * Run a script body that prints JSON, and return it parsed — null when the\n * script printed nothing. Output that isn't JSON is a SCRIPT_FAILED carrying\n * the script, never a bare SyntaxError.\n */\nexport async function runPowerShellJson(body: string, budget: RunBudget): Promise<unknown> {\n const { stdout, script, durationMs } = await execute(body, budget);\n const output = stdout.trim();\n if (output === '' || output === 'null') return null;\n try {\n return JSON.parse(output);\n } catch (error) {\n throw new ScriptError({\n runner: RUNNER,\n script,\n stderr: '',\n durationMs,\n message: `PowerShell printed output that is not valid JSON: ${output.slice(0, 200)}`,\n cause: error,\n });\n }\n}\n\n/**\n * Write text a script will read back, and hand back its cleanup.\n *\n * Large HTML — an email body, a Word-sized template — goes through a file rather\n * than the script itself, where it would have to be escaped and would bloat\n * every error and debug event that carries the script.\n */\nexport function scriptInput(kind: string, contents: string): { path: string; cleanup(): void } {\n const file = tempFile(kind, 'html');\n fs.writeFileSync(file, contents, 'utf8');\n return { path: file, cleanup: () => removeTempFile(file) };\n}\n\n/**\n * Read a file a script wrote, as UTF-8, and delete it; '' when it never\n * appeared. .NET's UTF-8 writer opens the file with a byte-order mark, which is\n * not part of the text.\n */\nexport function readScriptOutput(file: string): string {\n try {\n return fs.readFileSync(file, 'utf8').replace(/^\\uFEFF/, '');\n } catch {\n return '';\n } finally {\n removeTempFile(file);\n }\n}\n"]}
|
|
@@ -43,13 +43,13 @@ export declare const DELIVERY_STORE_PS = "\n$store = if ($account -ne $null) { $
|
|
|
43
43
|
*/
|
|
44
44
|
export declare const NAMED_STORE_PS: string;
|
|
45
45
|
/**
|
|
46
|
-
* Resolve one email into
|
|
46
|
+
* Resolve one email into `$item`, or fail as NOT_FOUND.
|
|
47
47
|
*
|
|
48
48
|
* The StoreID is tried first, since it lets an id from any mounted mailbox
|
|
49
49
|
* resolve; the bare lookup (default store) is the fallback for a caller holding
|
|
50
50
|
* a stale StoreID or none.
|
|
51
51
|
*/
|
|
52
|
-
export declare function itemLookupScript(email: EmailLocator
|
|
52
|
+
export declare function itemLookupScript(email: EmailLocator): string;
|
|
53
53
|
/**
|
|
54
54
|
* The sender's SMTP address as `$senderSmtp`. Requires `$item`.
|
|
55
55
|
*
|
|
@@ -71,11 +71,11 @@ export declare const FIND_FOLDER_PS = "\nfunction Find-FolderByName($root, [stri
|
|
|
71
71
|
*/
|
|
72
72
|
export declare function cutoffScript(daysBack: number): string;
|
|
73
73
|
/**
|
|
74
|
-
*
|
|
74
|
+
* `$filtered` = the items of `$<folderVariable>` with `property` on or after
|
|
75
75
|
* `$cutoff`. Requires `cutoffScript`. A store that rejects the filter outright
|
|
76
76
|
* yields every item instead, which the per-item re-check then narrows.
|
|
77
77
|
*/
|
|
78
|
-
export declare function itemsSinceScript(folderVariable: string, property: string
|
|
78
|
+
export declare function itemsSinceScript(folderVariable: string, property: string): string;
|
|
79
79
|
/**
|
|
80
80
|
* Resolve `$scope` from `$store`: a well-known root walked down through any
|
|
81
81
|
* further segments. Also binds `$scopeCreated`.
|
package/dist/windows/scripts.js
CHANGED
|
@@ -91,22 +91,22 @@ $store = $storeFolder.Store
|
|
|
91
91
|
$storeId = $store.StoreID
|
|
92
92
|
`;
|
|
93
93
|
/**
|
|
94
|
-
* Resolve one email into
|
|
94
|
+
* Resolve one email into `$item`, or fail as NOT_FOUND.
|
|
95
95
|
*
|
|
96
96
|
* The StoreID is tried first, since it lets an id from any mounted mailbox
|
|
97
97
|
* resolve; the bare lookup (default store) is the fallback for a caller holding
|
|
98
98
|
* a stale StoreID or none.
|
|
99
99
|
*/
|
|
100
|
-
function itemLookupScript(email
|
|
100
|
+
function itemLookupScript(email) {
|
|
101
101
|
const byStore = email.storeId
|
|
102
|
-
? `try {
|
|
102
|
+
? `try { $item = $ns.GetItemFromID($lookupId, ${(0, run_1.psString)(email.storeId)}) } catch { $item = $null }`
|
|
103
103
|
: '';
|
|
104
104
|
return `
|
|
105
105
|
$lookupId = ${(0, run_1.psString)(email.entryId)}
|
|
106
|
-
|
|
106
|
+
$item = $null
|
|
107
107
|
${byStore}
|
|
108
|
-
if (
|
|
109
|
-
if (
|
|
108
|
+
if ($item -eq $null) { try { $item = $ns.GetItemFromID($lookupId) } catch { $item = $null } }
|
|
109
|
+
if ($item -eq $null) {
|
|
110
110
|
throw "${(0, errors_1.failureTag)('NOT_FOUND', 'email')}No email found for entry id '$lookupId'. It may have been moved or deleted - list the mail again for a current id."
|
|
111
111
|
}
|
|
112
112
|
`;
|
|
@@ -155,14 +155,14 @@ $cutoffFilter = $cutoff.ToString('g').Replace("'", "''")
|
|
|
155
155
|
`;
|
|
156
156
|
}
|
|
157
157
|
/**
|
|
158
|
-
*
|
|
158
|
+
* `$filtered` = the items of `$<folderVariable>` with `property` on or after
|
|
159
159
|
* `$cutoff`. Requires `cutoffScript`. A store that rejects the filter outright
|
|
160
160
|
* yields every item instead, which the per-item re-check then narrows.
|
|
161
161
|
*/
|
|
162
|
-
function itemsSinceScript(folderVariable, property
|
|
162
|
+
function itemsSinceScript(folderVariable, property) {
|
|
163
163
|
return `
|
|
164
|
-
|
|
165
|
-
try {
|
|
164
|
+
$filtered = $null
|
|
165
|
+
try { $filtered = $${folderVariable}.Items.Restrict("[${property}] >= '$cutoffFilter'") } catch { $filtered = $${folderVariable}.Items }
|
|
166
166
|
`;
|
|
167
167
|
}
|
|
168
168
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scripts.js","sourceRoot":"","sources":["../../src/windows/scripts.ts"],"names":[],"mappings":";;;AAuCA,sCAoBC;AAsCD,4CAaC;AAyCD,oCAKC;AAOD,4CAKC;AAYD,0CAmCC;AAvND,0EAA0E;AAC1E,EAAE;AACF,uEAAuE;AACvE,+EAA+E;AAC/E,8EAA8E;AAC9E,2CAA2C;AAC3C,+BAAwC;AACxC,sCAAuC;AAGvC;;;;;;GAMG;AACU,QAAA,UAAU,GAAG;;;;CAIzB,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,SAAgB,aAAa,CAAC,YAAoB;IAC9C,OAAO,GAAG,kBAAU;YACZ,IAAA,cAAQ,EAAC,YAAY,CAAC;;;;;;;;;;;;;;;aAerB,IAAA,mBAAU,EAAC,mBAAmB,CAAC;;CAE3C,CAAC;AACF,CAAC;AAED;;;;;;GAMG;AACU,QAAA,iBAAiB,GAAG;;;CAGhC,CAAC;AAEF;;;;;;;;GAQG;AACU,QAAA,cAAc,GAAG;;aAEjB,IAAA,mBAAU,EAAC,mBAAmB,CAAC;;;;CAI3C,CAAC;AAEF;;;;;;GAMG;AACH,SAAgB,gBAAgB,CAAC,KAAmB,EAAE,QAAQ,GAAG,MAAM;IACnE,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO;QACzB,CAAC,CAAC,UAAU,QAAQ,mCAAmC,IAAA,cAAQ,EAAC,KAAK,CAAC,OAAO,CAAC,gBAAgB,QAAQ,YAAY;QAClH,CAAC,CAAC,EAAE,CAAC;IACT,OAAO;cACG,IAAA,cAAQ,EAAC,KAAK,CAAC,OAAO,CAAC;GAClC,QAAQ;EACT,OAAO;OACF,QAAQ,wBAAwB,QAAQ,8CAA8C,QAAQ;OAC9F,QAAQ;aACF,IAAA,mBAAU,EAAC,WAAW,EAAE,OAAO,CAAC;;CAE5C,CAAC;AACF,CAAC;AAED;;;;;GAKG;AACU,QAAA,cAAc,GAAG;;;;;;CAM7B,CAAC;AAEF,oGAAoG;AACvF,QAAA,cAAc,GAAG;;;;;;;;;;;;CAY7B,CAAC;AAEF;;;;;;;;;GASG;AACH,SAAgB,YAAY,CAAC,QAAgB;IACzC,OAAO;gCACqB,IAAA,WAAK,EAAC,QAAQ,CAAC;;CAE9C,CAAC;AACF,CAAC;AAED;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,cAAsB,EAAE,QAAgB,EAAE,QAAgB;IACvF,OAAO;GACR,QAAQ;SACF,QAAQ,OAAO,cAAc,qBAAqB,QAAQ,qCAAqC,QAAQ,OAAO,cAAc;CACpI,CAAC;AACF,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,eAAe,CAAC,GAAkB,EAAE,WAAmB,EAAE,aAAa,GAAG,KAAK;IAC1F,MAAM,IAAI,GAAG,2BAA2B,IAAA,WAAK,EAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC;IAC7D,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,YAAY,IAAI,0BAA0B,CAAC;IACjF,OAAO;EACT,sBAAc;iBACC,IAAA,cAAQ,EAAC,WAAW,CAAC;gBACtB,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,cAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;eACtC,IAAI;;;;;;;;;;;;;4BAaS,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ;;;;;;;;;;;;;aAajD,IAAA,mBAAU,EAAC,WAAW,EAAE,QAAQ,CAAC;EAC5C,CAAC;AACH,CAAC","sourcesContent":["// The PowerShell fragments more than one Windows operation is built from.\n//\n// A fragment lives here once two modules emit it; one used by a single\n// operation stays beside that operation, where its rules read next to the code\n// relying on them. Every fragment follows run.ts's escaping rule: caller text\n// only ever appears as a psString literal.\nimport { psInt, psString } from './run';\nimport { failureTag } from '../errors';\nimport type { EmailLocator, MailFolderRef } from '../types';\n\n/**\n * Open the MAPI session, binding `$outlook` and `$ns`.\n *\n * `Logon()` is a no-op against an Outlook that is already running and starts\n * one that isn't, which removes a class of \"works only while Outlook is open\"\n * failure.\n */\nexport const SESSION_PS = `\n$outlook = New-Object -ComObject Outlook.Application\n$ns = $outlook.GetNamespace('MAPI')\n$ns.Logon()\n`;\n\n/**\n * The session plus the mailbox an address names; binds `$target`, `$account`\n * and `$storeFolder`.\n *\n * An address can name a mailbox two ways, and only one is an Account. A mailbox\n * mounted as a secondary store — a shared mailbox, or one added after Outlook\n * started — has no Account object, so both are resolved and EITHER is enough:\n * `$account` is null for a store-only mailbox, and the operations that need an\n * identity to act as (sending) check for it themselves.\n *\n * Never falls back to the default account: a bad address must not silently\n * read or send from the wrong mailbox.\n *\n * The store is matched on the address BEFORE the account's display name, so a\n * profile where the two differ selects the same folders it always has.\n */\nexport function accountScript(emailAccount: string): string {\n return `${SESSION_PS}\n$target = ${psString(emailAccount)}\n$account = $null\nforeach ($a in $ns.Accounts) {\n if ($a.SmtpAddress -ieq $target) { $account = $a; break }\n}\n$storeFolder = $null\nforeach ($f in $ns.Folders) {\n if ($f.Name -ieq $target) { $storeFolder = $f; break }\n}\nif ($storeFolder -eq $null -and $account -ne $null) {\n foreach ($f in $ns.Folders) {\n if ($f.Name -ieq $account.DisplayName) { $storeFolder = $f; break }\n }\n}\nif ($account -eq $null -and $storeFolder -eq $null) {\n throw \"${failureTag('ACCOUNT_NOT_FOUND')}Account '$target' not found in this Outlook profile.\"\n}\n`;\n}\n\n/**\n * The mailbox's store as `$store` (and its id as `$storeId`), reached through\n * the account's delivery store. Requires `accountScript`.\n *\n * Falls back to the resolved store folder, because a store-only mailbox has no\n * account to read a delivery store off.\n */\nexport const DELIVERY_STORE_PS = `\n$store = if ($account -ne $null) { $account.DeliveryStore } else { $storeFolder.Store }\n$storeId = $store.StoreID\n`;\n\n/**\n * The mailbox's store as `$store`, reached through the namespace's top-level\n * folder for it. Requires `accountScript`.\n *\n * Kept beside DELIVERY_STORE_PS rather than replaced by it: the readers resolve\n * their store this way, and on a profile where a mailbox is open under a display\n * name that differs from its delivery store's, the two routes do not select the\n * same folders. Each operation keeps the route it was verified against.\n */\nexport const NAMED_STORE_PS = `\nif ($storeFolder -eq $null) {\n throw \"${failureTag('ACCOUNT_NOT_FOUND')}Account '$target' has no mailbox open in this Outlook profile.\"\n}\n$store = $storeFolder.Store\n$storeId = $store.StoreID\n`;\n\n/**\n * Resolve one email into `$<variable>`, or fail as NOT_FOUND.\n *\n * The StoreID is tried first, since it lets an id from any mounted mailbox\n * resolve; the bare lookup (default store) is the fallback for a caller holding\n * a stale StoreID or none.\n */\nexport function itemLookupScript(email: EmailLocator, variable = 'item'): string {\n const byStore = email.storeId\n ? `try { $${variable} = $ns.GetItemFromID($lookupId, ${psString(email.storeId)}) } catch { $${variable} = $null }`\n : '';\n return `\n$lookupId = ${psString(email.entryId)}\n$${variable} = $null\n${byStore}\nif ($${variable} -eq $null) { try { $${variable} = $ns.GetItemFromID($lookupId) } catch { $${variable} = $null } }\nif ($${variable} -eq $null) {\n throw \"${failureTag('NOT_FOUND', 'email')}No email found for entry id '$lookupId'. It may have been moved or deleted - list the mail again for a current id.\"\n}\n`;\n}\n\n/**\n * The sender's SMTP address as `$senderSmtp`. Requires `$item`.\n *\n * An Exchange sender's SenderEmailAddress is an X.500 DN, not an address; a\n * caller replying to it or matching it against anything needs the address.\n */\nexport const SENDER_SMTP_PS = `\n$senderSmtp = ''\ntry {\n if ($item.SenderEmailType -eq 'EX') { $senderSmtp = [string]$item.Sender.GetExchangeUser().PrimarySmtpAddress }\n} catch {}\nif (-not $senderSmtp) { try { $senderSmtp = [string]$item.SenderEmailAddress } catch {} }\n`;\n\n/** Recursive, case-insensitive folder search by name, depth-capped so a huge tree can't hang it. */\nexport const FIND_FOLDER_PS = `\nfunction Find-FolderByName($root, [string]$name, [int]$depth) {\n foreach ($f in $root.Folders) {\n if ($f.Name -ieq $name) { return $f }\n }\n if ($depth -le 1) { return $null }\n foreach ($f in $root.Folders) {\n $hit = Find-FolderByName $f $name ($depth - 1)\n if ($hit -ne $null) { return $hit }\n }\n return $null\n}\n`;\n\n/**\n * `$cutoff`, the moment `daysBack` days ago, and `$cutoffFilter`, that moment\n * as a date string for an `Items.Restrict` filter.\n *\n * Restrict parses date strings in the machine's own regional format, so the\n * string is formatted in it ('g': short date and time) rather than a fixed\n * pattern — 'MM/dd/yyyy' reads as a different day on a day-first locale. Every\n * reader re-checks each item against `$cutoff` too, so the window holds even\n * where a store reads the filter loosely.\n */\nexport function cutoffScript(daysBack: number): string {\n return `\n$cutoff = (Get-Date).AddDays(-${psInt(daysBack)})\n$cutoffFilter = $cutoff.ToString('g').Replace(\"'\", \"''\")\n`;\n}\n\n/**\n * `$<variable>` = the items of `$<folderVariable>` with `property` on or after\n * `$cutoff`. Requires `cutoffScript`. A store that rejects the filter outright\n * yields every item instead, which the per-item re-check then narrows.\n */\nexport function itemsSinceScript(folderVariable: string, property: string, variable: string): string {\n return `\n$${variable} = $null\ntry { $${variable} = $${folderVariable}.Items.Restrict(\"[${property}] >= '$cutoffFilter'\") } catch { $${variable} = $${folderVariable}.Items }\n`;\n}\n\n/**\n * Resolve `$scope` from `$store`: a well-known root walked down through any\n * further segments. Also binds `$scopeCreated`.\n *\n * Segments match direct children; a bare name that isn't a direct child falls\n * back to a recursive search, so one folder string works across operations. A\n * missing folder fails as NOT_FOUND naming what the caller typed and what the\n * deepest folder reached does hold — never a silent fall back to the root. With\n * `createMissing`, the whole missing chain is created instead.\n */\nexport function mailScopeScript(ref: MailFolderRef, folderLabel: string, createMissing = false): string {\n const root = `$store.GetDefaultFolder(${psInt(ref.rootId)})`;\n if (ref.segments.length === 0) return `$scope = ${root}\\n$scopeCreated = $false`;\n return `\n${FIND_FOLDER_PS}\n$folderLabel = ${psString(folderLabel)}\n$segments = @(${ref.segments.map(psString).join(', ')})\n$scopeRoot = ${root}\n$scope = $scopeRoot\n$deepest = $scopeRoot\n$scopeCreated = $false\nforeach ($seg in $segments) {\n $next = $null\n foreach ($f in $scope.Folders) { if ($f.Name -ieq $seg) { $next = $f; break } }\n if ($next -eq $null) { $deepest = $scope; $scope = $null; break }\n $scope = $next\n}\nif ($scope -eq $null -and $segments.Count -eq 1) {\n $scope = Find-FolderByName $scopeRoot $segments[0] 3\n}\nif ($scope -eq $null -and ${createMissing ? '$true' : '$false'}) {\n # Rebuild the chain from the root, creating only what is genuinely absent.\n $scope = $scopeRoot\n foreach ($seg in $segments) {\n $next = $null\n foreach ($f in $scope.Folders) { if ($f.Name -ieq $seg) { $next = $f; break } }\n if ($next -eq $null) { $next = $scope.Folders.Add($seg); $scopeCreated = $true }\n $scope = $next\n }\n}\nif ($scope -eq $null) {\n $names = @()\n foreach ($f in $deepest.Folders) { $names += $f.Name }\n throw \"${failureTag('NOT_FOUND', 'folder')}Folder '$folderLabel' not found. Folders under '$($deepest.Name)': $($names -join ', ')\"\n}`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"scripts.js","sourceRoot":"","sources":["../../src/windows/scripts.ts"],"names":[],"mappings":";;;AAuCA,sCAoBC;AAsCD,4CAaC;AAyCD,oCAKC;AAOD,4CAKC;AAYD,0CAmCC;AAvND,0EAA0E;AAC1E,EAAE;AACF,uEAAuE;AACvE,+EAA+E;AAC/E,8EAA8E;AAC9E,2CAA2C;AAC3C,+BAAwC;AACxC,sCAAuC;AAGvC;;;;;;GAMG;AACU,QAAA,UAAU,GAAG;;;;CAIzB,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,SAAgB,aAAa,CAAC,YAAoB;IAC9C,OAAO,GAAG,kBAAU;YACZ,IAAA,cAAQ,EAAC,YAAY,CAAC;;;;;;;;;;;;;;;aAerB,IAAA,mBAAU,EAAC,mBAAmB,CAAC;;CAE3C,CAAC;AACF,CAAC;AAED;;;;;;GAMG;AACU,QAAA,iBAAiB,GAAG;;;CAGhC,CAAC;AAEF;;;;;;;;GAQG;AACU,QAAA,cAAc,GAAG;;aAEjB,IAAA,mBAAU,EAAC,mBAAmB,CAAC;;;;CAI3C,CAAC;AAEF;;;;;;GAMG;AACH,SAAgB,gBAAgB,CAAC,KAAmB;IAChD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO;QACzB,CAAC,CAAC,8CAA8C,IAAA,cAAQ,EAAC,KAAK,CAAC,OAAO,CAAC,6BAA6B;QACpG,CAAC,CAAC,EAAE,CAAC;IACT,OAAO;cACG,IAAA,cAAQ,EAAC,KAAK,CAAC,OAAO,CAAC;;EAEnC,OAAO;;;aAGI,IAAA,mBAAU,EAAC,WAAW,EAAE,OAAO,CAAC;;CAE5C,CAAC;AACF,CAAC;AAED;;;;;GAKG;AACU,QAAA,cAAc,GAAG;;;;;;CAM7B,CAAC;AAEF,oGAAoG;AACvF,QAAA,cAAc,GAAG;;;;;;;;;;;;CAY7B,CAAC;AAEF;;;;;;;;;GASG;AACH,SAAgB,YAAY,CAAC,QAAgB;IACzC,OAAO;gCACqB,IAAA,WAAK,EAAC,QAAQ,CAAC;;CAE9C,CAAC;AACF,CAAC;AAED;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,cAAsB,EAAE,QAAgB;IACrE,OAAO;;qBAEU,cAAc,qBAAqB,QAAQ,iDAAiD,cAAc;CAC9H,CAAC;AACF,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,eAAe,CAAC,GAAkB,EAAE,WAAmB,EAAE,aAAa,GAAG,KAAK;IAC1F,MAAM,IAAI,GAAG,2BAA2B,IAAA,WAAK,EAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC;IAC7D,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,YAAY,IAAI,0BAA0B,CAAC;IACjF,OAAO;EACT,sBAAc;iBACC,IAAA,cAAQ,EAAC,WAAW,CAAC;gBACtB,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,cAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;eACtC,IAAI;;;;;;;;;;;;;4BAaS,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ;;;;;;;;;;;;;aAajD,IAAA,mBAAU,EAAC,WAAW,EAAE,QAAQ,CAAC;EAC5C,CAAC;AACH,CAAC","sourcesContent":["// The PowerShell fragments more than one Windows operation is built from.\n//\n// A fragment lives here once two modules emit it; one used by a single\n// operation stays beside that operation, where its rules read next to the code\n// relying on them. Every fragment follows run.ts's escaping rule: caller text\n// only ever appears as a psString literal.\nimport { psInt, psString } from './run';\nimport { failureTag } from '../errors';\nimport type { EmailLocator, MailFolderRef } from '../types';\n\n/**\n * Open the MAPI session, binding `$outlook` and `$ns`.\n *\n * `Logon()` is a no-op against an Outlook that is already running and starts\n * one that isn't, which removes a class of \"works only while Outlook is open\"\n * failure.\n */\nexport const SESSION_PS = `\n$outlook = New-Object -ComObject Outlook.Application\n$ns = $outlook.GetNamespace('MAPI')\n$ns.Logon()\n`;\n\n/**\n * The session plus the mailbox an address names; binds `$target`, `$account`\n * and `$storeFolder`.\n *\n * An address can name a mailbox two ways, and only one is an Account. A mailbox\n * mounted as a secondary store — a shared mailbox, or one added after Outlook\n * started — has no Account object, so both are resolved and EITHER is enough:\n * `$account` is null for a store-only mailbox, and the operations that need an\n * identity to act as (sending) check for it themselves.\n *\n * Never falls back to the default account: a bad address must not silently\n * read or send from the wrong mailbox.\n *\n * The store is matched on the address BEFORE the account's display name, so a\n * profile where the two differ selects the same folders it always has.\n */\nexport function accountScript(emailAccount: string): string {\n return `${SESSION_PS}\n$target = ${psString(emailAccount)}\n$account = $null\nforeach ($a in $ns.Accounts) {\n if ($a.SmtpAddress -ieq $target) { $account = $a; break }\n}\n$storeFolder = $null\nforeach ($f in $ns.Folders) {\n if ($f.Name -ieq $target) { $storeFolder = $f; break }\n}\nif ($storeFolder -eq $null -and $account -ne $null) {\n foreach ($f in $ns.Folders) {\n if ($f.Name -ieq $account.DisplayName) { $storeFolder = $f; break }\n }\n}\nif ($account -eq $null -and $storeFolder -eq $null) {\n throw \"${failureTag('ACCOUNT_NOT_FOUND')}Account '$target' not found in this Outlook profile.\"\n}\n`;\n}\n\n/**\n * The mailbox's store as `$store` (and its id as `$storeId`), reached through\n * the account's delivery store. Requires `accountScript`.\n *\n * Falls back to the resolved store folder, because a store-only mailbox has no\n * account to read a delivery store off.\n */\nexport const DELIVERY_STORE_PS = `\n$store = if ($account -ne $null) { $account.DeliveryStore } else { $storeFolder.Store }\n$storeId = $store.StoreID\n`;\n\n/**\n * The mailbox's store as `$store`, reached through the namespace's top-level\n * folder for it. Requires `accountScript`.\n *\n * Kept beside DELIVERY_STORE_PS rather than replaced by it: the readers resolve\n * their store this way, and on a profile where a mailbox is open under a display\n * name that differs from its delivery store's, the two routes do not select the\n * same folders. Each operation keeps the route it was verified against.\n */\nexport const NAMED_STORE_PS = `\nif ($storeFolder -eq $null) {\n throw \"${failureTag('ACCOUNT_NOT_FOUND')}Account '$target' has no mailbox open in this Outlook profile.\"\n}\n$store = $storeFolder.Store\n$storeId = $store.StoreID\n`;\n\n/**\n * Resolve one email into `$item`, or fail as NOT_FOUND.\n *\n * The StoreID is tried first, since it lets an id from any mounted mailbox\n * resolve; the bare lookup (default store) is the fallback for a caller holding\n * a stale StoreID or none.\n */\nexport function itemLookupScript(email: EmailLocator): string {\n const byStore = email.storeId\n ? `try { $item = $ns.GetItemFromID($lookupId, ${psString(email.storeId)}) } catch { $item = $null }`\n : '';\n return `\n$lookupId = ${psString(email.entryId)}\n$item = $null\n${byStore}\nif ($item -eq $null) { try { $item = $ns.GetItemFromID($lookupId) } catch { $item = $null } }\nif ($item -eq $null) {\n throw \"${failureTag('NOT_FOUND', 'email')}No email found for entry id '$lookupId'. It may have been moved or deleted - list the mail again for a current id.\"\n}\n`;\n}\n\n/**\n * The sender's SMTP address as `$senderSmtp`. Requires `$item`.\n *\n * An Exchange sender's SenderEmailAddress is an X.500 DN, not an address; a\n * caller replying to it or matching it against anything needs the address.\n */\nexport const SENDER_SMTP_PS = `\n$senderSmtp = ''\ntry {\n if ($item.SenderEmailType -eq 'EX') { $senderSmtp = [string]$item.Sender.GetExchangeUser().PrimarySmtpAddress }\n} catch {}\nif (-not $senderSmtp) { try { $senderSmtp = [string]$item.SenderEmailAddress } catch {} }\n`;\n\n/** Recursive, case-insensitive folder search by name, depth-capped so a huge tree can't hang it. */\nexport const FIND_FOLDER_PS = `\nfunction Find-FolderByName($root, [string]$name, [int]$depth) {\n foreach ($f in $root.Folders) {\n if ($f.Name -ieq $name) { return $f }\n }\n if ($depth -le 1) { return $null }\n foreach ($f in $root.Folders) {\n $hit = Find-FolderByName $f $name ($depth - 1)\n if ($hit -ne $null) { return $hit }\n }\n return $null\n}\n`;\n\n/**\n * `$cutoff`, the moment `daysBack` days ago, and `$cutoffFilter`, that moment\n * as a date string for an `Items.Restrict` filter.\n *\n * Restrict parses date strings in the machine's own regional format, so the\n * string is formatted in it ('g': short date and time) rather than a fixed\n * pattern — 'MM/dd/yyyy' reads as a different day on a day-first locale. Every\n * reader re-checks each item against `$cutoff` too, so the window holds even\n * where a store reads the filter loosely.\n */\nexport function cutoffScript(daysBack: number): string {\n return `\n$cutoff = (Get-Date).AddDays(-${psInt(daysBack)})\n$cutoffFilter = $cutoff.ToString('g').Replace(\"'\", \"''\")\n`;\n}\n\n/**\n * `$filtered` = the items of `$<folderVariable>` with `property` on or after\n * `$cutoff`. Requires `cutoffScript`. A store that rejects the filter outright\n * yields every item instead, which the per-item re-check then narrows.\n */\nexport function itemsSinceScript(folderVariable: string, property: string): string {\n return `\n$filtered = $null\ntry { $filtered = $${folderVariable}.Items.Restrict(\"[${property}] >= '$cutoffFilter'\") } catch { $filtered = $${folderVariable}.Items }\n`;\n}\n\n/**\n * Resolve `$scope` from `$store`: a well-known root walked down through any\n * further segments. Also binds `$scopeCreated`.\n *\n * Segments match direct children; a bare name that isn't a direct child falls\n * back to a recursive search, so one folder string works across operations. A\n * missing folder fails as NOT_FOUND naming what the caller typed and what the\n * deepest folder reached does hold — never a silent fall back to the root. With\n * `createMissing`, the whole missing chain is created instead.\n */\nexport function mailScopeScript(ref: MailFolderRef, folderLabel: string, createMissing = false): string {\n const root = `$store.GetDefaultFolder(${psInt(ref.rootId)})`;\n if (ref.segments.length === 0) return `$scope = ${root}\\n$scopeCreated = $false`;\n return `\n${FIND_FOLDER_PS}\n$folderLabel = ${psString(folderLabel)}\n$segments = @(${ref.segments.map(psString).join(', ')})\n$scopeRoot = ${root}\n$scope = $scopeRoot\n$deepest = $scopeRoot\n$scopeCreated = $false\nforeach ($seg in $segments) {\n $next = $null\n foreach ($f in $scope.Folders) { if ($f.Name -ieq $seg) { $next = $f; break } }\n if ($next -eq $null) { $deepest = $scope; $scope = $null; break }\n $scope = $next\n}\nif ($scope -eq $null -and $segments.Count -eq 1) {\n $scope = Find-FolderByName $scopeRoot $segments[0] 3\n}\nif ($scope -eq $null -and ${createMissing ? '$true' : '$false'}) {\n # Rebuild the chain from the root, creating only what is genuinely absent.\n $scope = $scopeRoot\n foreach ($seg in $segments) {\n $next = $null\n foreach ($f in $scope.Folders) { if ($f.Name -ieq $seg) { $next = $f; break } }\n if ($next -eq $null) { $next = $scope.Folders.Add($seg); $scopeCreated = $true }\n $scope = $next\n }\n}\nif ($scope -eq $null) {\n $names = @()\n foreach ($f in $deepest.Folders) { $names += $f.Name }\n throw \"${failureTag('NOT_FOUND', 'folder')}Folder '$folderLabel' not found. Folders under '$($deepest.Name)': $($names -join ', ')\"\n}`;\n}\n"]}
|
package/dist/windows/send.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { ReplyRequest, SendRequest } from '../backend';
|
|
2
|
-
import type { ReplyEmailResult } from '../types';
|
|
3
|
-
/**
|
|
4
|
-
|
|
2
|
+
import type { ReplyEmailResult, SendEmailResult } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Send, display or file a new email. Only a filed draft reports an id: a sent
|
|
5
|
+
* item has left for the Outbox, and a displayed one is saved nowhere yet. The
|
|
6
|
+
* id is read after `Save()`, which is what assigns it — the EntryID the
|
|
7
|
+
* draft then keeps, and the one listOutlookDrafts reports.
|
|
8
|
+
*/
|
|
9
|
+
export declare function sendOutlookEmail(request: SendRequest): Promise<SendEmailResult>;
|
|
5
10
|
/** Reply to an email, with the composed HTML above the quoted original. */
|
|
6
11
|
export declare function replyOutlookEmail(request: ReplyRequest): Promise<ReplyEmailResult>;
|
package/dist/windows/send.js
CHANGED
|
@@ -45,7 +45,12 @@ function dispose(variable, disposition) {
|
|
|
45
45
|
function recipientLine(entries) {
|
|
46
46
|
return (0, run_1.psString)(entries.join('; '));
|
|
47
47
|
}
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Send, display or file a new email. Only a filed draft reports an id: a sent
|
|
50
|
+
* item has left for the Outbox, and a displayed one is saved nowhere yet. The
|
|
51
|
+
* id is read after `Save()`, which is what assigns it — the EntryID the
|
|
52
|
+
* draft then keeps, and the one listOutlookDrafts reports.
|
|
53
|
+
*/
|
|
49
54
|
async function sendOutlookEmail(request) {
|
|
50
55
|
// The body goes through a file: inline, a large email bloats the script and
|
|
51
56
|
// every error that carries it.
|
|
@@ -53,8 +58,9 @@ async function sendOutlookEmail(request) {
|
|
|
53
58
|
const attach = request.attachments
|
|
54
59
|
.map(file => `[void]$mail.Attachments.Add(${(0, run_1.psString)(file)})`)
|
|
55
60
|
.join('\n');
|
|
61
|
+
const draftEntryId = request.disposition === 'save' ? '[string]$mail.EntryID' : "''";
|
|
56
62
|
try {
|
|
57
|
-
await (0, run_1.
|
|
63
|
+
const output = await (0, run_1.runPowerShellJson)(`${(0, scripts_1.accountScript)(request.account)}
|
|
58
64
|
$mail = $outlook.CreateItem(0)
|
|
59
65
|
${sendUsingAccount('mail')}
|
|
60
66
|
$mail.To = ${recipientLine(request.to)}
|
|
@@ -64,7 +70,9 @@ $mail.Subject = ${(0, run_1.psString)(request.subject)}
|
|
|
64
70
|
$mail.HTMLBody = [IO.File]::ReadAllText(${(0, run_1.psString)(body.path)}, [Text.Encoding]::UTF8)
|
|
65
71
|
${attach}
|
|
66
72
|
${dispose('mail', request.disposition)}
|
|
73
|
+
ConvertTo-Json -Compress @{ draftEntryId = ${draftEntryId} }
|
|
67
74
|
`, 'standard');
|
|
75
|
+
return { draftEntryId: (0, json_1.str)((0, json_1.record)(output).draftEntryId) };
|
|
68
76
|
}
|
|
69
77
|
finally {
|
|
70
78
|
body.cleanup();
|
package/dist/windows/send.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"send.js","sourceRoot":"","sources":["../../src/windows/send.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"send.js","sourceRoot":"","sources":["../../src/windows/send.ts"],"names":[],"mappings":";;AAuDA,4CAyBC;AAGD,8CA6BC;AAhHD,wEAAwE;AACxE,+BAAiE;AACjE,uCAA4E;AAC5E,mDAA0D;AAC1D,sCAAuC;AACvC,yCAA6C;AAI7C;;;;;;;;;;;;GAYG;AACH,SAAS,gBAAgB,CAAC,QAAgB;IACtC,OAAO;aACE,IAAA,mBAAU,EAAC,iBAAiB,CAAC;;SAEjC,QAAQ,+FAA+F,QAAQ,gBAAgB,CAAC;AACzI,CAAC;AAED;;;GAGG;AACH,SAAS,OAAO,CAAC,QAAgB,EAAE,WAAwB;IACvD,QAAQ,WAAW,EAAE,CAAC;QAClB,KAAK,MAAM;YACP,OAAO,IAAI,QAAQ,SAAS,CAAC;QACjC,KAAK,SAAS;YACV,OAAO,IAAI,QAAQ,YAAY,CAAC;QACpC,KAAK,MAAM;YACP,OAAO,IAAI,QAAQ,SAAS,CAAC;IACrC,CAAC;AACL,CAAC;AAED,gEAAgE;AAChE,SAAS,aAAa,CAAC,OAA0B;IAC7C,OAAO,IAAA,cAAQ,EAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AACxC,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,gBAAgB,CAAC,OAAoB;IACvD,4EAA4E;IAC5E,+BAA+B;IAC/B,MAAM,IAAI,GAAG,IAAA,iBAAW,EAAC,YAAY,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IACzD,MAAM,MAAM,GAAG,OAAO,CAAC,WAAW;SAC7B,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,+BAA+B,IAAA,cAAQ,EAAC,IAAI,CAAC,GAAG,CAAC;SAC7D,IAAI,CAAC,IAAI,CAAC,CAAC;IAChB,MAAM,YAAY,GAAG,OAAO,CAAC,WAAW,KAAK,MAAM,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,IAAI,CAAC;IACrF,IAAI,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,IAAA,uBAAiB,EAAC,GAAG,IAAA,uBAAa,EAAC,OAAO,CAAC,OAAO,CAAC;;EAE9E,gBAAgB,CAAC,MAAM,CAAC;aACb,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC;aACzB,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC;cACxB,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC;kBACtB,IAAA,cAAQ,EAAC,OAAO,CAAC,OAAO,CAAC;0CACD,IAAA,cAAQ,EAAC,IAAI,CAAC,IAAI,CAAC;EAC3D,MAAM;EACN,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC;6CACO,YAAY;CACxD,EAAE,UAAU,CAAC,CAAC;QACP,OAAO,EAAE,YAAY,EAAE,IAAA,UAAG,EAAC,IAAA,aAAM,EAAC,MAAM,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC;IAC9D,CAAC;YAAS,CAAC;QACP,IAAI,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;AACL,CAAC;AAED,2EAA2E;AACpE,KAAK,UAAU,iBAAiB,CAAC,OAAqB;IACzD,MAAM,MAAM,GAAG,IAAA,iBAAW,EAAC,cAAc,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,IAAI,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,IAAA,uBAAiB,EAAC,GAAG,IAAA,uBAAa,EAAC,OAAO,CAAC,OAAO,CAAC;EAC9E,IAAA,0BAAgB,EAAC,OAAO,CAAC,KAAK,CAAC;iBAChB,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO;EACtD,gBAAgB,CAAC,OAAO,CAAC;uCACY,IAAA,cAAQ,EAAC,MAAM,CAAC,IAAI,CAAC;YAChD,IAAA,cAAQ,EAAC,+BAAmB,CAAC;;;;;;;;;;;;EAYvC,wBAAc;EACd,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC;;CAEtC,EAAE,UAAU,CAAC,CAAC;QACP,MAAM,MAAM,GAAG,IAAA,aAAM,EAAC,MAAM,CAAC,CAAC;QAC9B,OAAO,EAAE,EAAE,EAAE,IAAA,UAAG,EAAC,MAAM,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,IAAA,UAAG,EAAC,MAAM,CAAC,OAAO,CAAC,EAAE,eAAe,EAAE,IAAA,UAAG,EAAC,MAAM,CAAC,eAAe,CAAC,EAAE,CAAC;IAC9G,CAAC;YAAS,CAAC;QACP,MAAM,CAAC,OAAO,EAAE,CAAC;IACrB,CAAC;AACL,CAAC","sourcesContent":["// Composing outgoing mail: a new email, and a reply to an existing one.\nimport { psString, runPowerShellJson, scriptInput } from './run';\nimport { accountScript, itemLookupScript, SENDER_SMTP_PS } from './scripts';\nimport { WORD_SECTION_ANCHOR } from '../shared/replyBody';\nimport { failureTag } from '../errors';\nimport { record, str } from '../shared/json';\nimport type { Disposition, ReplyRequest, SendRequest } from '../backend';\nimport type { ReplyEmailResult, SendEmailResult } from '../types';\n\n/**\n * Set the sending account through IDispatch reflection.\n *\n * Plain assignment (`$mail.SendUsingAccount = $account`) is a silent no-op under\n * PowerShell's COM binding — the mail then goes out from whichever account\n * Outlook considers default, which is exactly what this package must never do.\n *\n * Refuses a mailbox that resolved to a store with no Account: SendUsingAccount\n * takes an Account, and sending with none falls through to the default account.\n * The usual cause is a stale session — Outlook builds its Accounts list at\n * startup, so a mailbox added since has its store mounted and readable but no\n * Account until Outlook restarts — and the message says so.\n */\nfunction sendUsingAccount(variable: string): string {\n return `if ($account -eq $null) {\n throw \"${failureTag('INVALID_REQUEST')}Mailbox '$target' has no sending account in this Outlook session. If it was added to the profile after Outlook started, restart Outlook and try again.\"\n}\n[void]$${variable}.GetType().InvokeMember('SendUsingAccount', [Reflection.BindingFlags]::SetProperty, $null, $${variable}, @($account))`;\n}\n\n/**\n * What to do with a composed item. `Display()` only shows a compose window and\n * never files the item, so a windowless draft has to `Save()` into Drafts.\n */\nfunction dispose(variable: string, disposition: Disposition): string {\n switch (disposition) {\n case 'send':\n return `$${variable}.Send()`;\n case 'display':\n return `$${variable}.Display()`;\n case 'save':\n return `$${variable}.Save()`;\n }\n}\n\n/** Outlook takes recipients as one semicolon-separated line. */\nfunction recipientLine(entries: readonly string[]): string {\n return psString(entries.join('; '));\n}\n\n/**\n * Send, display or file a new email. Only a filed draft reports an id: a sent\n * item has left for the Outbox, and a displayed one is saved nowhere yet. The\n * id is read after `Save()`, which is what assigns it — the EntryID the\n * draft then keeps, and the one listOutlookDrafts reports.\n */\nexport async function sendOutlookEmail(request: SendRequest): Promise<SendEmailResult> {\n // The body goes through a file: inline, a large email bloats the script and\n // every error that carries it.\n const body = scriptInput('email-body', request.htmlBody);\n const attach = request.attachments\n .map(file => `[void]$mail.Attachments.Add(${psString(file)})`)\n .join('\\n');\n const draftEntryId = request.disposition === 'save' ? '[string]$mail.EntryID' : \"''\";\n try {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n$mail = $outlook.CreateItem(0)\n${sendUsingAccount('mail')}\n$mail.To = ${recipientLine(request.to)}\n$mail.CC = ${recipientLine(request.cc)}\n$mail.BCC = ${recipientLine(request.bcc)}\n$mail.Subject = ${psString(request.subject)}\n$mail.HTMLBody = [IO.File]::ReadAllText(${psString(body.path)}, [Text.Encoding]::UTF8)\n${attach}\n${dispose('mail', request.disposition)}\nConvertTo-Json -Compress @{ draftEntryId = ${draftEntryId} }\n`, 'standard');\n return { draftEntryId: str(record(output).draftEntryId) };\n } finally {\n body.cleanup();\n }\n}\n\n/** Reply to an email, with the composed HTML above the quoted original. */\nexport async function replyOutlookEmail(request: ReplyRequest): Promise<ReplyEmailResult> {\n const insert = scriptInput('reply-insert', request.html);\n try {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${itemLookupScript(request.email)}\n$reply = $item.${request.replyAll ? 'ReplyAll' : 'Reply'}()\n${sendUsingAccount('reply')}\n$insertHtml = [IO.File]::ReadAllText(${psString(insert.path)}, [Text.Encoding]::UTF8)\n$anchor = ${psString(WORD_SECTION_ANCHOR)}\n# Read once: every HTMLBody read marshals the whole generated reply across COM,\n# and a reply on a long thread runs to hundreds of KB.\n$html = [string]$reply.HTMLBody\n$at = $html.IndexOf($anchor)\nif ($at -ge 0) { $at += $anchor.Length } else {\n $bodyTag = [regex]::Match($html, '<body[^>]*>')\n $at = if ($bodyTag.Success) { $bodyTag.Index + $bodyTag.Length } else { 0 }\n}\n$reply.HTMLBody = $html.Insert($at, $insertHtml)\n$to = [string]$reply.To\n$subject = [string]$reply.Subject\n${SENDER_SMTP_PS}\n${dispose('reply', request.disposition)}\nConvertTo-Json -Compress @{ to = $to; subject = $subject; repliedToSender = $senderSmtp }\n`, 'standard');\n const result = record(output);\n return { to: str(result.to), subject: str(result.subject), repliedToSender: str(result.repliedToSender) };\n } finally {\n insert.cleanup();\n }\n}\n"]}
|
|
@@ -8,16 +8,11 @@ exports.editEmailTemplate = editEmailTemplate;
|
|
|
8
8
|
const run_1 = require("./run");
|
|
9
9
|
const scripts_1 = require("./scripts");
|
|
10
10
|
const json_1 = require("../shared/json");
|
|
11
|
+
const templateLimits_1 = require("../shared/templateLimits");
|
|
11
12
|
const templateBody_1 = require("../templateBody");
|
|
12
13
|
const mail_1 = require("../mail");
|
|
13
14
|
const errors_1 = require("../errors");
|
|
14
15
|
const runtime_1 = require("../runtime");
|
|
15
|
-
/** How deep under the mailbox root a template folder is searched for. */
|
|
16
|
-
const TEMPLATE_SEARCH_DEPTH = 3;
|
|
17
|
-
/** Cap on one template body; a Word-generated one is large, and a runaway one shouldn't sink the call. */
|
|
18
|
-
const MAX_TEMPLATE_HTML = 100_000;
|
|
19
|
-
/** Plain text standing in for a body that wasn't asked for. */
|
|
20
|
-
const PREVIEW_CHARS = 200;
|
|
21
16
|
/**
|
|
22
17
|
* The template emails in a mailbox folder, newest first. A missing folder is
|
|
23
18
|
* reported as folderFound:false with the mailbox's top-level folder names
|
|
@@ -28,7 +23,7 @@ async function readTemplateEmails(request) {
|
|
|
28
23
|
${scripts_1.DELIVERY_STORE_PS}
|
|
29
24
|
$root = $store.GetRootFolder()
|
|
30
25
|
${scripts_1.FIND_FOLDER_PS}
|
|
31
|
-
$folder = Find-FolderByName $root ${(0, run_1.psString)(request.folder)} ${(0, run_1.psInt)(TEMPLATE_SEARCH_DEPTH)}
|
|
26
|
+
$folder = Find-FolderByName $root ${(0, run_1.psString)(request.folder)} ${(0, run_1.psInt)(templateLimits_1.TEMPLATE_SEARCH_DEPTH)}
|
|
32
27
|
if ($folder -eq $null) {
|
|
33
28
|
$names = @()
|
|
34
29
|
foreach ($f in $root.Folders) { $names += [string]$f.Name }
|
|
@@ -38,8 +33,8 @@ if ($folder -eq $null) {
|
|
|
38
33
|
$includeBody = ${(0, run_1.psBool)(request.includeBody)}
|
|
39
34
|
$wanted = ${(0, run_1.psString)(request.subject ?? '')}
|
|
40
35
|
$limit = ${(0, run_1.psInt)(request.limit)}
|
|
41
|
-
$maxHtml = ${(0, run_1.psInt)(MAX_TEMPLATE_HTML)}
|
|
42
|
-
$previewChars = ${(0, run_1.psInt)(
|
|
36
|
+
$maxHtml = ${(0, run_1.psInt)(templateLimits_1.MAX_TEMPLATE_HTML)}
|
|
37
|
+
$previewChars = ${(0, run_1.psInt)(templateLimits_1.TEMPLATE_PREVIEW_CHARS)}
|
|
43
38
|
$items = $folder.Items
|
|
44
39
|
$items.Sort('[LastModificationTime]', $true)
|
|
45
40
|
$rows = @()
|
|
@@ -61,10 +56,9 @@ for ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {
|
|
|
61
56
|
$markers = ''
|
|
62
57
|
if ($includeBody) {
|
|
63
58
|
try { $html = [string]$item.HTMLBody } catch {}
|
|
64
|
-
# Embedded (cid:) images
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
$html = [regex]::Replace($html, '<img[^>]*src="cid:[^"]*"[^>]*>', '')
|
|
59
|
+
# Embedded (cid:) images are attachments of the TEMPLATE item, so they
|
|
60
|
+
# would show as broken placeholders anywhere else.
|
|
61
|
+
$html = [regex]::Replace($html, ${(0, run_1.psString)(templateLimits_1.EMBEDDED_IMAGE_SOURCE)}, '', [Text.RegularExpressions.RegexOptions]::IgnoreCase)
|
|
68
62
|
if ($html.Length -gt $maxHtml) { $html = $html.Substring(0, $maxHtml) }
|
|
69
63
|
} else {
|
|
70
64
|
# No body asked for: a short preview, plus the section and placeholder
|
|
@@ -74,7 +68,7 @@ for ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {
|
|
|
74
68
|
$plain = ''
|
|
75
69
|
try { $plain = [string]$item.Body } catch {}
|
|
76
70
|
$found = @()
|
|
77
|
-
foreach ($m in [regex]::Matches($plain,
|
|
71
|
+
foreach ($m in [regex]::Matches($plain, ${(0, run_1.psString)(templateBody_1.MARKER_TOKEN_SOURCE)})) {
|
|
78
72
|
$token = $m.Value -replace '\\s', ''
|
|
79
73
|
if ($found -notcontains $token) { $found += $token }
|
|
80
74
|
}
|
|
@@ -136,7 +130,7 @@ ${scripts_1.DELIVERY_STORE_PS}
|
|
|
136
130
|
$root = $store.GetRootFolder()
|
|
137
131
|
${scripts_1.FIND_FOLDER_PS}
|
|
138
132
|
$folderName = ${(0, run_1.psString)(request.folder)}
|
|
139
|
-
$folder = Find-FolderByName $root $folderName ${(0, run_1.psInt)(TEMPLATE_SEARCH_DEPTH)}
|
|
133
|
+
$folder = Find-FolderByName $root $folderName ${(0, run_1.psInt)(templateLimits_1.TEMPLATE_SEARCH_DEPTH)}
|
|
140
134
|
$folderCreated = $false
|
|
141
135
|
if ($folder -eq $null) {
|
|
142
136
|
$folder = $root.Folders.Add($folderName)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.js","sourceRoot":"","sources":["../../src/windows/templates.ts"],"names":[],"mappings":"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","sourcesContent":["// Template emails: ordinary mail items kept in a mailbox folder and reused as\n// email bodies, plus the compose-window editor for one.\nimport { psBool, psInt, psString, readScriptOutput, runPowerShell, runPowerShellJson, scriptInput } from './run';\nimport { accountScript, DELIVERY_STORE_PS, FIND_FOLDER_PS, SESSION_PS } from './scripts';\nimport { bool, record, str, toArray } from '../shared/json';\nimport { findTemplateMarkers } from '../templateBody';\nimport { FolderId } from '../mail';\nimport { NotFoundError } from '../errors';\nimport { tempFile } from '../runtime';\nimport type { EditTemplateRequest, ReadTemplatesRequest, SaveTemplateRequest } from '../backend';\nimport type { SaveTemplateResult, TemplateFolderResult } from '../types';\n\n/** How deep under the mailbox root a template folder is searched for. */\nconst TEMPLATE_SEARCH_DEPTH = 3;\n\n/** Cap on one template body; a Word-generated one is large, and a runaway one shouldn't sink the call. */\nconst MAX_TEMPLATE_HTML = 100_000;\n\n/** Plain text standing in for a body that wasn't asked for. */\nconst PREVIEW_CHARS = 200;\n\n/**\n * The template emails in a mailbox folder, newest first. A missing folder is\n * reported as folderFound:false with the mailbox's top-level folder names\n * rather than thrown, so a caller can offer to create it.\n */\nexport async function readTemplateEmails(request: ReadTemplatesRequest): Promise<TemplateFolderResult> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n$root = $store.GetRootFolder()\n${FIND_FOLDER_PS}\n$folder = Find-FolderByName $root ${psString(request.folder)} ${psInt(TEMPLATE_SEARCH_DEPTH)}\nif ($folder -eq $null) {\n $names = @()\n foreach ($f in $root.Folders) { $names += [string]$f.Name }\n ConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{ folderFound = $false; folderPath = ''; templates = @(); availableFolders = @($names) })\n exit 0\n}\n$includeBody = ${psBool(request.includeBody)}\n$wanted = ${psString(request.subject ?? '')}\n$limit = ${psInt(request.limit)}\n$maxHtml = ${psInt(MAX_TEMPLATE_HTML)}\n$previewChars = ${psInt(PREVIEW_CHARS)}\n$items = $folder.Items\n$items.Sort('[LastModificationTime]', $true)\n$rows = @()\n$count = $items.Count\n# Without a subject the sort does the capping, newest first. With one, the whole\n# folder is scanned — the template wanted need not be among the newest.\nfor ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {\n $item = $null\n try { $item = $items.Item($i) } catch { continue }\n if ($item -eq $null) { continue }\n $class = 0\n try { $class = [int]$item.Class } catch {}\n if ($class -ne 43) { continue }\n $subject = ''\n try { $subject = ([string]$item.Subject).Trim() } catch {}\n if ($wanted -and $subject -ine $wanted) { continue }\n $html = ''\n $preview = ''\n $markers = ''\n if ($includeBody) {\n try { $html = [string]$item.HTMLBody } catch {}\n # Embedded (cid:) images live as attachments on the TEMPLATE item, so its\n # HTML reused elsewhere shows broken-image placeholders. Template emails\n # are text and markup only.\n $html = [regex]::Replace($html, '<img[^>]*src=\"cid:[^\"]*\"[^>]*>', '')\n if ($html.Length -gt $maxHtml) { $html = $html.Substring(0, $maxHtml) }\n } else {\n # No body asked for: a short preview, plus the section and placeholder\n # markers scanned here from the full plain text — so a caller can confirm\n # a template is intact without its body crossing stdout. Word can split a\n # marker across tags in the HTML but not in the plain text.\n $plain = ''\n try { $plain = [string]$item.Body } catch {}\n $found = @()\n foreach ($m in [regex]::Matches($plain, '\\\\[\\\\[\\\\s*/?\\\\s*[A-Za-z0-9_-]{1,40}\\\\s*\\\\]\\\\]|\\\\{\\\\{\\\\s*[A-Za-z0-9_-]{1,40}\\\\s*\\\\}\\\\}')) {\n $token = $m.Value -replace '\\\\s', ''\n if ($found -notcontains $token) { $found += $token }\n }\n $markers = $found -join ' '\n $preview = $plain.Trim()\n if ($preview.Length -gt $previewChars) { $preview = $preview.Substring(0, $previewChars) }\n }\n $modified = ''\n try { $modified = $item.LastModificationTime.ToString('yyyy-MM-dd HH:mm') } catch {}\n $rows += [PSCustomObject]@{\n entryId = [string]$item.EntryID\n subject = $subject\n htmlBody = $html\n bodyPreview = $preview\n markers = $markers\n lastModified = $modified\n }\n}\nConvertTo-Json -Depth 4 -InputObject ([PSCustomObject]@{\n folderFound = $true\n folderPath = [string]$folder.FolderPath\n templates = @($rows)\n availableFolders = @()\n})\n`, 'standard');\n const e = record(output);\n return {\n folderFound: bool(e.folderFound),\n folderPath: str(e.folderPath),\n templates: toArray(e.templates).map(row => {\n const t = record(row);\n const html = str(t.htmlBody);\n // With the HTML in hand it is authoritative (markers survive Word\n // splitting them across tags); without it, the markers the script\n // scanned out of the plain text stand in.\n const markers = findTemplateMarkers(request.includeBody ? html : str(t.markers));\n return {\n entryId: str(t.entryId),\n subject: str(t.subject),\n htmlBody: html,\n bodyPreview: str(t.bodyPreview),\n sections: markers.sections,\n placeholders: markers.placeholders,\n lastModified: str(t.lastModified),\n };\n }),\n availableFolders: toArray(e.availableFolders).map(str),\n };\n}\n\n/**\n * Save a new template email into a folder, creating the folder at the mailbox\n * root when absent. Always adds a new item — never overwrites one.\n */\nexport async function saveTemplateEmail(request: SaveTemplateRequest): Promise<SaveTemplateResult> {\n const body = scriptInput('template-body', request.htmlBody);\n try {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n$root = $store.GetRootFolder()\n${FIND_FOLDER_PS}\n$folderName = ${psString(request.folder)}\n$folder = Find-FolderByName $root $folderName ${psInt(TEMPLATE_SEARCH_DEPTH)}\n$folderCreated = $false\nif ($folder -eq $null) {\n $folder = $root.Folders.Add($folderName)\n $folderCreated = $true\n}\n$mail = $outlook.CreateItem(0)\n$mail.Subject = ${psString(request.subject)}\n$mail.HTMLBody = [IO.File]::ReadAllText(${psString(body.path)}, [Text.Encoding]::UTF8)\n$mail.Save()\n[void]$mail.Move($folder)\nConvertTo-Json -Compress -InputObject ([PSCustomObject]@{ folderPath = [string]$folder.FolderPath; folderCreated = $folderCreated })\n`, 'standard');\n const e = record(output);\n return {folderPath: str(e.folderPath), folderCreated: bool(e.folderCreated)};\n } finally {\n body.cleanup();\n }\n}\n\n/**\n * Open a compose window pre-filled with the HTML, wait for it to close, and\n * return what was saved (Ctrl+S) — or, failing a save, the body as it last\n * stood while the window was open. Leftover editor drafts are removed.\n */\nexport async function editEmailTemplate(request: EditTemplateRequest): Promise<string> {\n const input = scriptInput('template-input', request.html);\n const outputFile = tempFile('template-output', 'html');\n try {\n await runPowerShell(`${SESSION_PS}\n$subject = ${psString(`${request.label} - Save (Ctrl+S) and close when done`)}\n$outputPath = ${psString(outputFile)}\n$mail = $outlook.CreateItem(0)\n$mail.Subject = $subject\n$mail.HTMLBody = [IO.File]::ReadAllText(${psString(input.path)}, [Text.Encoding]::UTF8)\n$mail.Display()\nStart-Sleep -Seconds 2\n# Poll until the compose window closes, keeping the last live body as the\n# fallback for an edit that was never saved.\n$liveBody = ''\nwhile ($true) {\n Start-Sleep -Milliseconds 700\n $open = $false\n try {\n foreach ($inspector in $outlook.Inspectors) {\n $current = $null\n try { $current = $inspector.CurrentItem } catch {}\n if ($current -ne $null -and [string]$current.Subject -eq $subject) {\n $open = $true\n try { $liveBody = [string]$current.HTMLBody } catch {}\n break\n }\n }\n } catch {}\n if (-not $open) { break }\n}\n# Prefer the saved draft: it is what the person chose to keep, and it outlives\n# the window. Every editor draft is removed so none pile up in Drafts.\n$savedBody = ''\ntry {\n $draftItems = $ns.GetDefaultFolder(${psInt(FolderId.Drafts)}).Items\n for ($i = $draftItems.Count; $i -ge 1; $i--) {\n $draft = $draftItems.Item($i)\n if ([string]$draft.Subject -eq $subject) {\n if (-not $savedBody) { try { $savedBody = [string]$draft.HTMLBody } catch {} }\n try { $draft.Delete() } catch {}\n }\n }\n} catch {}\n$finalBody = if ($savedBody) { $savedBody } else { $liveBody }\nif ($finalBody) { [IO.File]::WriteAllText($outputPath, $finalBody, [Text.Encoding]::UTF8) }\n`, 'interactive');\n } finally {\n input.cleanup();\n }\n const saved = readScriptOutput(outputFile);\n if (!saved) {\n throw new NotFoundError('template', 'No template was saved. Save (Ctrl+S) before closing the window.');\n }\n return saved;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"templates.js","sourceRoot":"","sources":["../../src/windows/templates.ts"],"names":[],"mappings":"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","sourcesContent":["// Template emails: ordinary mail items kept in a mailbox folder and reused as\n// email bodies, plus the compose-window editor for one.\nimport { psBool, psInt, psString, readScriptOutput, runPowerShell, runPowerShellJson, scriptInput } from './run';\nimport { accountScript, DELIVERY_STORE_PS, FIND_FOLDER_PS, SESSION_PS } from './scripts';\nimport { bool, record, str, toArray } from '../shared/json';\nimport {\n EMBEDDED_IMAGE_SOURCE,\n MAX_TEMPLATE_HTML,\n TEMPLATE_PREVIEW_CHARS,\n TEMPLATE_SEARCH_DEPTH,\n} from '../shared/templateLimits';\nimport { findTemplateMarkers, MARKER_TOKEN_SOURCE } from '../templateBody';\nimport { FolderId } from '../mail';\nimport { NotFoundError } from '../errors';\nimport { tempFile } from '../runtime';\nimport type { EditTemplateRequest, ReadTemplatesRequest, SaveTemplateRequest } from '../backend';\nimport type { SaveTemplateResult, TemplateFolderResult } from '../types';\n\n/**\n * The template emails in a mailbox folder, newest first. A missing folder is\n * reported as folderFound:false with the mailbox's top-level folder names\n * rather than thrown, so a caller can offer to create it.\n */\nexport async function readTemplateEmails(request: ReadTemplatesRequest): Promise<TemplateFolderResult> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n$root = $store.GetRootFolder()\n${FIND_FOLDER_PS}\n$folder = Find-FolderByName $root ${psString(request.folder)} ${psInt(TEMPLATE_SEARCH_DEPTH)}\nif ($folder -eq $null) {\n $names = @()\n foreach ($f in $root.Folders) { $names += [string]$f.Name }\n ConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{ folderFound = $false; folderPath = ''; templates = @(); availableFolders = @($names) })\n exit 0\n}\n$includeBody = ${psBool(request.includeBody)}\n$wanted = ${psString(request.subject ?? '')}\n$limit = ${psInt(request.limit)}\n$maxHtml = ${psInt(MAX_TEMPLATE_HTML)}\n$previewChars = ${psInt(TEMPLATE_PREVIEW_CHARS)}\n$items = $folder.Items\n$items.Sort('[LastModificationTime]', $true)\n$rows = @()\n$count = $items.Count\n# Without a subject the sort does the capping, newest first. With one, the whole\n# folder is scanned — the template wanted need not be among the newest.\nfor ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {\n $item = $null\n try { $item = $items.Item($i) } catch { continue }\n if ($item -eq $null) { continue }\n $class = 0\n try { $class = [int]$item.Class } catch {}\n if ($class -ne 43) { continue }\n $subject = ''\n try { $subject = ([string]$item.Subject).Trim() } catch {}\n if ($wanted -and $subject -ine $wanted) { continue }\n $html = ''\n $preview = ''\n $markers = ''\n if ($includeBody) {\n try { $html = [string]$item.HTMLBody } catch {}\n # Embedded (cid:) images are attachments of the TEMPLATE item, so they\n # would show as broken placeholders anywhere else.\n $html = [regex]::Replace($html, ${psString(EMBEDDED_IMAGE_SOURCE)}, '', [Text.RegularExpressions.RegexOptions]::IgnoreCase)\n if ($html.Length -gt $maxHtml) { $html = $html.Substring(0, $maxHtml) }\n } else {\n # No body asked for: a short preview, plus the section and placeholder\n # markers scanned here from the full plain text — so a caller can confirm\n # a template is intact without its body crossing stdout. Word can split a\n # marker across tags in the HTML but not in the plain text.\n $plain = ''\n try { $plain = [string]$item.Body } catch {}\n $found = @()\n foreach ($m in [regex]::Matches($plain, ${psString(MARKER_TOKEN_SOURCE)})) {\n $token = $m.Value -replace '\\\\s', ''\n if ($found -notcontains $token) { $found += $token }\n }\n $markers = $found -join ' '\n $preview = $plain.Trim()\n if ($preview.Length -gt $previewChars) { $preview = $preview.Substring(0, $previewChars) }\n }\n $modified = ''\n try { $modified = $item.LastModificationTime.ToString('yyyy-MM-dd HH:mm') } catch {}\n $rows += [PSCustomObject]@{\n entryId = [string]$item.EntryID\n subject = $subject\n htmlBody = $html\n bodyPreview = $preview\n markers = $markers\n lastModified = $modified\n }\n}\nConvertTo-Json -Depth 4 -InputObject ([PSCustomObject]@{\n folderFound = $true\n folderPath = [string]$folder.FolderPath\n templates = @($rows)\n availableFolders = @()\n})\n`, 'standard');\n const e = record(output);\n return {\n folderFound: bool(e.folderFound),\n folderPath: str(e.folderPath),\n templates: toArray(e.templates).map(row => {\n const t = record(row);\n const html = str(t.htmlBody);\n // With the HTML in hand it is authoritative (markers survive Word\n // splitting them across tags); without it, the markers the script\n // scanned out of the plain text stand in.\n const markers = findTemplateMarkers(request.includeBody ? html : str(t.markers));\n return {\n entryId: str(t.entryId),\n subject: str(t.subject),\n htmlBody: html,\n bodyPreview: str(t.bodyPreview),\n sections: markers.sections,\n placeholders: markers.placeholders,\n lastModified: str(t.lastModified),\n };\n }),\n availableFolders: toArray(e.availableFolders).map(str),\n };\n}\n\n/**\n * Save a new template email into a folder, creating the folder at the mailbox\n * root when absent. Always adds a new item — never overwrites one.\n */\nexport async function saveTemplateEmail(request: SaveTemplateRequest): Promise<SaveTemplateResult> {\n const body = scriptInput('template-body', request.htmlBody);\n try {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n$root = $store.GetRootFolder()\n${FIND_FOLDER_PS}\n$folderName = ${psString(request.folder)}\n$folder = Find-FolderByName $root $folderName ${psInt(TEMPLATE_SEARCH_DEPTH)}\n$folderCreated = $false\nif ($folder -eq $null) {\n $folder = $root.Folders.Add($folderName)\n $folderCreated = $true\n}\n$mail = $outlook.CreateItem(0)\n$mail.Subject = ${psString(request.subject)}\n$mail.HTMLBody = [IO.File]::ReadAllText(${psString(body.path)}, [Text.Encoding]::UTF8)\n$mail.Save()\n[void]$mail.Move($folder)\nConvertTo-Json -Compress -InputObject ([PSCustomObject]@{ folderPath = [string]$folder.FolderPath; folderCreated = $folderCreated })\n`, 'standard');\n const e = record(output);\n return { folderPath: str(e.folderPath), folderCreated: bool(e.folderCreated) };\n } finally {\n body.cleanup();\n }\n}\n\n/**\n * Open a compose window pre-filled with the HTML, wait for it to close, and\n * return what was saved (Ctrl+S) — or, failing a save, the body as it last\n * stood while the window was open. Leftover editor drafts are removed.\n */\nexport async function editEmailTemplate(request: EditTemplateRequest): Promise<string> {\n const input = scriptInput('template-input', request.html);\n const outputFile = tempFile('template-output', 'html');\n try {\n await runPowerShell(`${SESSION_PS}\n$subject = ${psString(`${request.label} - Save (Ctrl+S) and close when done`)}\n$outputPath = ${psString(outputFile)}\n$mail = $outlook.CreateItem(0)\n$mail.Subject = $subject\n$mail.HTMLBody = [IO.File]::ReadAllText(${psString(input.path)}, [Text.Encoding]::UTF8)\n$mail.Display()\nStart-Sleep -Seconds 2\n# Poll until the compose window closes, keeping the last live body as the\n# fallback for an edit that was never saved.\n$liveBody = ''\nwhile ($true) {\n Start-Sleep -Milliseconds 700\n $open = $false\n try {\n foreach ($inspector in $outlook.Inspectors) {\n $current = $null\n try { $current = $inspector.CurrentItem } catch {}\n if ($current -ne $null -and [string]$current.Subject -eq $subject) {\n $open = $true\n try { $liveBody = [string]$current.HTMLBody } catch {}\n break\n }\n }\n } catch {}\n if (-not $open) { break }\n}\n# Prefer the saved draft: it is what the person chose to keep, and it outlives\n# the window. Every editor draft is removed so none pile up in Drafts.\n$savedBody = ''\ntry {\n $draftItems = $ns.GetDefaultFolder(${psInt(FolderId.Drafts)}).Items\n for ($i = $draftItems.Count; $i -ge 1; $i--) {\n $draft = $draftItems.Item($i)\n if ([string]$draft.Subject -eq $subject) {\n if (-not $savedBody) { try { $savedBody = [string]$draft.HTMLBody } catch {} }\n try { $draft.Delete() } catch {}\n }\n }\n} catch {}\n$finalBody = if ($savedBody) { $savedBody } else { $liveBody }\nif ($finalBody) { [IO.File]::WriteAllText($outputPath, $finalBody, [Text.Encoding]::UTF8) }\n`, 'interactive');\n } finally {\n input.cleanup();\n }\n const saved = readScriptOutput(outputFile);\n if (!saved) {\n throw new NotFoundError('template', 'No template was saved. Save (Ctrl+S) before closing the window.');\n }\n return saved;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dawnlit/outlook-bridge",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Desktop Outlook automation for Node — read, search, send, reply, file, and template mail by driving the real Outlook client on Windows and macOS.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Dawnlit",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"prepare": "npm run build"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@types/node": "^24.
|
|
48
|
+
"@types/node": "^24.19.2",
|
|
49
49
|
"typescript": "~6.0.3"
|
|
50
50
|
}
|
|
51
51
|
}
|