@dawnlit/outlook-bridge 2.1.0 → 2.2.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 +11 -6
- package/dist/errors.d.ts +17 -6
- package/dist/errors.js +16 -7
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/mac/accounts.d.ts +4 -0
- package/dist/mac/accounts.js +10 -0
- package/dist/mac/accounts.js.map +1 -1
- package/dist/mac/bounces.js +10 -19
- package/dist/mac/bounces.js.map +1 -1
- package/dist/mac/cleanup.d.ts +11 -3
- package/dist/mac/cleanup.js +26 -17
- package/dist/mac/cleanup.js.map +1 -1
- package/dist/mac/drafts.js +3 -16
- package/dist/mac/drafts.js.map +1 -1
- package/dist/mac/folders.d.ts +5 -0
- package/dist/mac/folders.js +14 -5
- package/dist/mac/folders.js.map +1 -1
- package/dist/mac/profile.d.ts +12 -0
- package/dist/mac/profile.js +75 -1
- package/dist/mac/profile.js.map +1 -1
- package/dist/mac/read.js +14 -26
- package/dist/mac/read.js.map +1 -1
- package/dist/mac/run.d.ts +0 -3
- package/dist/mac/run.js +8 -8
- package/dist/mac/run.js.map +1 -1
- package/dist/mac/scripts.d.ts +46 -0
- package/dist/mac/scripts.js +98 -10
- package/dist/mac/scripts.js.map +1 -1
- package/dist/mac/send.d.ts +2 -1
- package/dist/mac/send.js +9 -3
- package/dist/mac/send.js.map +1 -1
- package/dist/mac/templates.js +1 -4
- package/dist/mac/templates.js.map +1 -1
- package/dist/mail.d.ts +6 -0
- package/dist/mail.js +7 -1
- package/dist/mail.js.map +1 -1
- package/dist/types.d.ts +20 -4
- package/dist/types.js.map +1 -1
- package/dist/windows/accounts.js +1 -1
- package/dist/windows/accounts.js.map +1 -1
- package/dist/windows/attachments.js +2 -2
- package/dist/windows/attachments.js.map +1 -1
- package/dist/windows/bounces.js +3 -3
- package/dist/windows/bounces.js.map +1 -1
- package/dist/windows/cleanup.js +2 -4
- package/dist/windows/cleanup.js.map +1 -1
- package/dist/windows/drafts.js +2 -2
- 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/json.js.map +1 -0
- package/dist/windows/read.js +17 -14
- package/dist/windows/read.js.map +1 -1
- package/dist/windows/scripts.d.ts +6 -0
- package/dist/windows/scripts.js +11 -2
- package/dist/windows/scripts.js.map +1 -1
- package/dist/windows/send.d.ts +5 -1
- package/dist/windows/send.js +14 -4
- package/dist/windows/send.js.map +1 -1
- package/dist/windows/templates.js +2 -2
- package/dist/windows/templates.js.map +1 -1
- package/package.json +1 -1
- package/dist/shared/json.js.map +0 -1
- /package/dist/{shared → windows}/json.d.ts +0 -0
- /package/dist/{shared → windows}/json.js +0 -0
package/dist/windows/read.js
CHANGED
|
@@ -9,7 +9,7 @@ exports.openOutlookEmail = openOutlookEmail;
|
|
|
9
9
|
// selected in the running Outlook.
|
|
10
10
|
const run_1 = require("./run");
|
|
11
11
|
const scripts_1 = require("./scripts");
|
|
12
|
-
const json_1 = require("
|
|
12
|
+
const json_1 = require("./json");
|
|
13
13
|
const mail_1 = require("../mail");
|
|
14
14
|
const errors_1 = require("../errors");
|
|
15
15
|
/**
|
|
@@ -28,6 +28,12 @@ function dateProperty(rootId) {
|
|
|
28
28
|
* escaping in two languages intact.
|
|
29
29
|
*/
|
|
30
30
|
const BODY_B64_PS = `[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes([string]$bodyRaw))`;
|
|
31
|
+
/**
|
|
32
|
+
* The item's attachment file names as `$attachmentNames`, unnamed ones
|
|
33
|
+
* included: a name's position is its attachment's. Requires `$item`.
|
|
34
|
+
*/
|
|
35
|
+
const ATTACHMENT_NAMES_PS = `$attachmentNames = @()
|
|
36
|
+
try { foreach ($att in $item.Attachments) { $attachmentNames += [string]$att.FileName } } catch {}`;
|
|
31
37
|
/** Recent mail from the Inbox root or one folder, newest first. */
|
|
32
38
|
async function readInboxEmails(request) {
|
|
33
39
|
const dateProp = dateProperty(request.folder.rootId);
|
|
@@ -57,8 +63,7 @@ for ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {
|
|
|
57
63
|
try { $bodyText = [string]$item.Body } catch {}
|
|
58
64
|
$preview = if ($bodyText.Length -gt $previewChars) { $bodyText.Substring(0, $previewChars) } else { $bodyText }
|
|
59
65
|
}
|
|
60
|
-
|
|
61
|
-
try { foreach ($att in $item.Attachments) { $attachmentNames += [string]$att.FileName } } catch {}
|
|
66
|
+
${ATTACHMENT_NAMES_PS}
|
|
62
67
|
$who = ''
|
|
63
68
|
$whoEmail = ''
|
|
64
69
|
if (${(0, run_1.psBool)(outgoing)}) {
|
|
@@ -76,7 +81,7 @@ for ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {
|
|
|
76
81
|
subject = ([string]$item.Subject).Trim()
|
|
77
82
|
senderName = $who
|
|
78
83
|
senderEmail = $whoEmail
|
|
79
|
-
receivedTime = if ($stamp -ne $null) { $
|
|
84
|
+
receivedTime = if ($stamp -ne $null) { ${(0, scripts_1.stampPs)('$stamp')} } else { '' }
|
|
80
85
|
bodyPreview = $preview
|
|
81
86
|
attachmentNames = @($attachmentNames)
|
|
82
87
|
}
|
|
@@ -118,11 +123,10 @@ if ($class -ne 43 -and $class -ne 46) {
|
|
|
118
123
|
}
|
|
119
124
|
$bodyRaw = ''
|
|
120
125
|
try { $bodyRaw = [string]$item.Body } catch {}
|
|
121
|
-
$
|
|
122
|
-
try { foreach ($att in $item.Attachments) { $attachmentNames += [string]$att.FileName } } catch {}
|
|
126
|
+
${ATTACHMENT_NAMES_PS}
|
|
123
127
|
${scripts_1.SENDER_SMTP_PS}
|
|
124
128
|
$received = ''
|
|
125
|
-
try { $received = $item.ReceivedTime
|
|
129
|
+
try { $received = ${(0, scripts_1.stampPs)('$item.ReceivedTime')} } catch {}
|
|
126
130
|
ConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{
|
|
127
131
|
entryId = [string]$item.EntryID
|
|
128
132
|
subject = ([string]$item.Subject).Trim()
|
|
@@ -254,8 +258,7 @@ for ($fx = 0; $fx -lt $folders.Count; $fx++) {
|
|
|
254
258
|
$received = $null
|
|
255
259
|
try { $received = $item.ReceivedTime } catch {}
|
|
256
260
|
if ($received -ne $null -and $received -lt $cutoff) { continue }
|
|
257
|
-
|
|
258
|
-
try { foreach ($att in $item.Attachments) { $attachmentNames += [string]$att.FileName } } catch {}
|
|
261
|
+
${ATTACHMENT_NAMES_PS}
|
|
259
262
|
if ($requireAttachment -and $attachmentNames.Count -eq 0) { continue }
|
|
260
263
|
$bodyRaw = ''
|
|
261
264
|
if ($includeBody) { try { $bodyRaw = [string]$item.Body } catch {} }
|
|
@@ -266,7 +269,7 @@ ${scripts_1.SENDER_SMTP_PS}
|
|
|
266
269
|
subject = $subject
|
|
267
270
|
senderName = [string]$item.SenderName
|
|
268
271
|
senderEmail = $senderSmtp
|
|
269
|
-
receivedTime = if ($received -ne $null) { $
|
|
272
|
+
receivedTime = if ($received -ne $null) { ${(0, scripts_1.stampPs)('$received')} } else { '' }
|
|
270
273
|
body = ${BODY_B64_PS}
|
|
271
274
|
attachmentNames = @($attachmentNames)
|
|
272
275
|
folderPath = $folderPath
|
|
@@ -311,8 +314,7 @@ if ($item -eq $null) {
|
|
|
311
314
|
if ([string]$item.MessageClass -notlike 'IPM.Note*') {
|
|
312
315
|
throw "${(0, errors_1.failureTag)('INVALID_REQUEST')}The item selected in Outlook is not an email."
|
|
313
316
|
}
|
|
314
|
-
$
|
|
315
|
-
try { foreach ($att in $item.Attachments) { $attachmentNames += [string]$att.FileName } } catch {}
|
|
317
|
+
${ATTACHMENT_NAMES_PS}
|
|
316
318
|
$bodyRaw = ''
|
|
317
319
|
try { $bodyRaw = [string]$item.Body } catch {}
|
|
318
320
|
$storeId = ''
|
|
@@ -321,7 +323,7 @@ $folderPath = ''
|
|
|
321
323
|
try { $folderPath = [string]$item.Parent.FolderPath } catch {}
|
|
322
324
|
${scripts_1.SENDER_SMTP_PS}
|
|
323
325
|
$received = ''
|
|
324
|
-
try { $received = $item.ReceivedTime
|
|
326
|
+
try { $received = ${(0, scripts_1.stampPs)('$item.ReceivedTime')} } catch {}
|
|
325
327
|
ConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{
|
|
326
328
|
entryId = [string]$item.EntryID
|
|
327
329
|
storeId = $storeId
|
|
@@ -343,7 +345,8 @@ ConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{
|
|
|
343
345
|
senderEmail: (0, json_1.str)(e.senderEmail),
|
|
344
346
|
receivedTime: (0, json_1.str)(e.receivedTime),
|
|
345
347
|
body: (0, json_1.base64Text)(e.body),
|
|
346
|
-
|
|
348
|
+
// Unnamed attachments keep their place, as in every other reader here.
|
|
349
|
+
attachmentNames: (0, json_1.toArray)(e.attachmentNames).map(json_1.str),
|
|
347
350
|
folderPath: (0, json_1.str)(e.folderPath),
|
|
348
351
|
};
|
|
349
352
|
}
|
package/dist/windows/read.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/windows/read.ts"],"names":[],"mappings":"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","sourcesContent":["// Reading mail: one folder, one item, a whole Inbox tree, or whatever is\n// selected in the running Outlook.\nimport { psArray, psBool, psInt, psString, runPowerShell, runPowerShellJson } from './run';\nimport {\n accountScript,\n cutoffScript,\n itemLookupScript,\n itemsSinceScript,\n mailScopeScript,\n NAMED_STORE_PS,\n SENDER_SMTP_PS,\n SESSION_PS,\n} from './scripts';\nimport { base64Text, record, str, strList, toArray } from '../shared/json';\nimport { FolderId, isOutgoingRoot, NON_INCOMING_ROOTS, REPLY_PREFIX_SOURCE, subjectGlobSource } from '../mail';\nimport { failureTag } from '../errors';\nimport type { RawEmail, ReadInboxRequest, SearchRequest } from '../backend';\nimport type { EmailLocator, InboxEmail, InboxSearchMatch, SelectedEmail } from '../types';\n\n/**\n * The COM property carrying each kind of folder's timestamp. Sent Items and\n * Drafts have no ReceivedTime, and filtering them on it returns an empty set\n * that looks exactly like an empty folder.\n */\nfunction dateProperty(rootId: number): string {\n if (rootId === FolderId.Drafts) return 'LastModificationTime';\n return isOutgoingRoot(rootId) ? 'SentOn' : 'ReceivedTime';\n}\n\n/**\n * Bodies cross the PowerShell boundary base64-encoded: they carry quotes,\n * control characters and non-ASCII that would otherwise have to survive JSON\n * escaping in two languages intact.\n */\nconst BODY_B64_PS = `[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes([string]$bodyRaw))`;\n\n/** Recent mail from the Inbox root or one folder, newest first. */\nexport async function readInboxEmails(request: ReadInboxRequest): Promise<InboxEmail[]> {\n const dateProp = dateProperty(request.folder.rootId);\n const outgoing = isOutgoingRoot(request.folder.rootId);\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${NAMED_STORE_PS}\n${mailScopeScript(request.folder, request.folderLabel)}\n$scopePath = [string]$scope.FolderPath\n${cutoffScript(request.daysBack)}\n${itemsSinceScript('scope', dateProp)}\n$filtered.Sort('[${dateProp}]', $true)\n$limit = ${psInt(request.limit)}\n$previewChars = ${psInt(request.previewChars)}\n$rows = @()\n$count = $filtered.Count\nfor ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {\n $item = $null\n try { $item = $filtered.Item($i) } catch {}\n if ($item -eq $null) { continue }\n $stamp = $null\n try { $stamp = $item.${dateProp} } catch {}\n # Sorted newest first, so the first item older than the window ends it.\n if ($stamp -ne $null -and $stamp -lt $cutoff) { break }\n $preview = ''\n if ($previewChars -gt 0) {\n $bodyText = ''\n try { $bodyText = [string]$item.Body } catch {}\n $preview = if ($bodyText.Length -gt $previewChars) { $bodyText.Substring(0, $previewChars) } else { $bodyText }\n }\n $attachmentNames = @()\n try { foreach ($att in $item.Attachments) { $attachmentNames += [string]$att.FileName } } catch {}\n $who = ''\n $whoEmail = ''\n if (${psBool(outgoing)}) {\n # Outgoing mail reports who it went TO; \"from\" is always the mailbox itself.\n try { $who = [string]$item.To } catch {}\n $whoEmail = $who\n } else {\n try { $who = [string]$item.SenderName } catch {}\n try { $whoEmail = [string]$item.SenderEmailAddress } catch {}\n }\n $rows += [PSCustomObject]@{\n entryId = [string]$item.EntryID\n storeId = $storeId\n folderPath = $scopePath\n subject = ([string]$item.Subject).Trim()\n senderName = $who\n senderEmail = $whoEmail\n receivedTime = if ($stamp -ne $null) { $stamp.ToString('yyyy-MM-dd HH:mm') } else { '' }\n bodyPreview = $preview\n attachmentNames = @($attachmentNames)\n }\n}\nConvertTo-Json -Depth 4 -InputObject @($rows)\n`, 'standard');\n return toArray(output).map(row => {\n const e = record(row);\n const attachmentNames = toArray(e.attachmentNames).map(str);\n return {\n entryId: str(e.entryId),\n storeId: str(e.storeId),\n subject: str(e.subject),\n senderName: str(e.senderName),\n senderEmail: str(e.senderEmail),\n receivedTime: str(e.receivedTime),\n bodyPreview: str(e.bodyPreview),\n attachmentNames,\n attachmentCount: attachmentNames.length,\n folderPath: str(e.folderPath),\n };\n });\n}\n\n/**\n * One email's full plain-text body, by id.\n *\n * `Body`, not `HTMLBody`: Outlook's own plain-text rendering is what every other\n * reader here consumes, and markup costs an order of magnitude more for the same\n * sentences. The tradeoff is that an HTML table's rows flatten.\n */\nexport async function readEmailBody(email: EmailLocator): Promise<RawEmail> {\n const output = await runPowerShellJson(`${SESSION_PS}\n${itemLookupScript(email)}\n$class = 0\ntry { $class = [int]$item.Class } catch {}\n# 43 = olMail, 46 = olReport (a non-delivery report is still worth reading).\nif ($class -ne 43 -and $class -ne 46) {\n throw \"${failureTag('NOT_FOUND', 'email')}The item with entry id '$lookupId' is not an email (class $class).\"\n}\n$bodyRaw = ''\ntry { $bodyRaw = [string]$item.Body } catch {}\n$attachmentNames = @()\ntry { foreach ($att in $item.Attachments) { $attachmentNames += [string]$att.FileName } } catch {}\n${SENDER_SMTP_PS}\n$received = ''\ntry { $received = $item.ReceivedTime.ToString('yyyy-MM-dd HH:mm') } catch {}\nConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{\n entryId = [string]$item.EntryID\n subject = ([string]$item.Subject).Trim()\n senderName = [string]$item.SenderName\n senderEmail = $senderSmtp\n receivedTime = $received\n body = ${BODY_B64_PS}\n attachmentNames = @($attachmentNames)\n})\n`, 'quick');\n const e = record(output);\n return {\n entryId: str(e.entryId) || email.entryId,\n subject: str(e.subject),\n senderName: str(e.senderName),\n senderEmail: str(e.senderEmail),\n receivedTime: str(e.receivedTime),\n body: base64Text(e.body),\n attachmentNames: toArray(e.attachmentNames).map(str),\n };\n}\n\n/**\n * Walk every folder under the Inbox for the mail matching the request.\n *\n * The window is enforced twice — a date Restrict per folder, then a per-item\n * check — because Restrict is not reliable across stores, and a scan that\n * silently widens on some mailboxes is the failure this guards against.\n * `subjectLike` also drives a server-side prefilter, purely as an optimization:\n * every subject is re-checked, so a store that can't run the query (IMAP stores\n * accept it and match nothing) returns the same set, only slower.\n */\nexport async function searchInboxByFilter(request: SearchRequest): Promise<InboxSearchMatch[]> {\n const subjectFiltered = !!(request.subjectLike || request.subjectPattern || request.excludeReplies);\n // The server-side prefilter speaks DASL, whose `like` wildcards are % and _.\n // A literal % or _ in the glob is left as a wildcard there: that can only\n // widen the prefilter, and the exact test below narrows it again.\n const subjectDasl = request.subjectLike ? request.subjectLike.replace(/\\*/g, '%').replace(/\\?/g, '_') : '';\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${NAMED_STORE_PS}\n$inbox = $store.GetDefaultFolder(${psInt(FolderId.Inbox)})\n$subjectLike = ${psString(request.subjectLike ? subjectGlobSource(request.subjectLike) : '')}\n$subjectDasl = ${psString(subjectDasl)}\n$subjectPattern = ${psString(request.subjectPattern?.source ?? '')}\n$replyPattern = ${psString(request.excludeReplies ? REPLY_PREFIX_SOURCE : '')}\n$requireSubject = ${psBool(subjectFiltered)}\n$requireAttachment = ${psBool(request.requireAttachment)}\n$includeBody = ${psBool(request.includeBody)}\n${cutoffScript(request.daysBack)}\n# Sent, Drafts, Deleted and Junk are siblings of the Inbox on an Exchange profile\n# and CHILDREN of it on an IMAP one. They are cut by id — their names are\n# localized — before the walk, so each takes its whole subtree with it.\n$skipIds = @{}\nforeach ($role in @(${NON_INCOMING_ROOTS.map(psInt).join(', ')})) {\n try {\n $roleFolder = $store.GetDefaultFolder($role)\n if ($roleFolder -ne $null) { $skipIds[[string]$roleFolder.EntryID] = $true }\n } catch {}\n}\n$skipNames = ${psArray(request.excludeFolders)}\n$onlyNames = ${psArray(request.includeFolders)}\nfunction Test-FolderNamed([string]$name, [string]$path, $list) {\n foreach ($entry in $list) {\n if ($name -ieq $entry -or $path -ieq $entry) { return $true }\n }\n return $false\n}\n# Traversal and scanning are separate, and the two lists are not symmetric: an\n# excluded folder is never queued, so its subtree goes with it, while an\n# included one only marks a folder the walk reached anyway — the walk still\n# passes through folders nobody named to reach a nested one somebody did.\n$rootName = ''\ntry { $rootName = [string]$inbox.Name } catch {}\n$rootPath = ''\ntry { $rootPath = [string]$inbox.FolderPath } catch {}\n$folders = [System.Collections.ArrayList]@($inbox)\n$inScope = [System.Collections.ArrayList]@(($onlyNames.Count -eq 0) -or (Test-FolderNamed $rootName $rootPath $onlyNames))\n$walked = 0\nwhile ($walked -lt $folders.Count) {\n try {\n foreach ($sub in $folders[$walked].Folders) {\n if ($skipIds.ContainsKey([string]$sub.EntryID)) { continue }\n $subName = ''\n try { $subName = [string]$sub.Name } catch {}\n $subPath = ''\n try { $subPath = [string]$sub.FolderPath } catch {}\n if (Test-FolderNamed $subName $subPath $skipNames) { continue }\n [void]$folders.Add($sub)\n [void]$inScope.Add(($onlyNames.Count -eq 0) -or (Test-FolderNamed $subName $subPath $onlyNames))\n }\n } catch {}\n $walked++\n}\n# The subject prefilter is DASL rather than Jet: some stores reject Jet's\n# \"[Subject] like\" outright, while DASL is accepted wherever Restrict is. The\n# two syntaxes can't share one filter, so it is a second Restrict on top of the\n# date one. The pattern is concatenated into the query, never interpolated.\n$subjectQuery = '@SQL=' + [char]34 + 'urn:schemas:httpmail:subject' + [char]34 + ' like ' + [char]39 + $subjectDasl.Replace(\"'\", \"''\") + [char]39\n# A list, not an array: += copies the whole array per row, and a wide scan returns thousands.\n$rows = New-Object System.Collections.ArrayList\n$seen = @{}\nfor ($fx = 0; $fx -lt $folders.Count; $fx++) {\n if (-not $inScope[$fx]) { continue }\n $folder = $folders[$fx]\n ${itemsSinceScript('folder', 'ReceivedTime').trim()}\n if ($subjectDasl) {\n try {\n $candidate = $filtered.Restrict($subjectQuery)\n if ($candidate.Count -gt 0) { $filtered = $candidate }\n } catch {}\n }\n $folderPath = ''\n try { $folderPath = [string]$folder.FolderPath } catch {}\n $count = $filtered.Count\n for ($i = 1; $i -le $count; $i++) {\n $item = $null\n try { $item = $filtered.Item($i) } catch {}\n # A restricted collection can hand back nothing for an index, and an item\n # with no id can't be acted on by anything this package does.\n if ($item -eq $null) { continue }\n $entryId = [string]$item.EntryID\n if (-not $entryId -or $seen.ContainsKey($entryId)) { continue }\n $seen[$entryId] = $true\n $subject = ''\n try { $subject = ([string]$item.Subject).Trim() } catch {}\n if ($requireSubject -and -not $subject) { continue }\n if ($subjectLike -and $subject -notmatch $subjectLike) { continue }\n if ($subjectPattern -and $subject -notmatch $subjectPattern) { continue }\n if ($replyPattern -and $subject -imatch $replyPattern) { continue }\n $received = $null\n try { $received = $item.ReceivedTime } catch {}\n if ($received -ne $null -and $received -lt $cutoff) { continue }\n $attachmentNames = @()\n try { foreach ($att in $item.Attachments) { $attachmentNames += [string]$att.FileName } } catch {}\n if ($requireAttachment -and $attachmentNames.Count -eq 0) { continue }\n $bodyRaw = ''\n if ($includeBody) { try { $bodyRaw = [string]$item.Body } catch {} }\n${SENDER_SMTP_PS}\n [void]$rows.Add([PSCustomObject]@{\n entryId = $entryId\n storeId = $storeId\n subject = $subject\n senderName = [string]$item.SenderName\n senderEmail = $senderSmtp\n receivedTime = if ($received -ne $null) { $received.ToString('yyyy-MM-dd HH:mm') } else { '' }\n body = ${BODY_B64_PS}\n attachmentNames = @($attachmentNames)\n folderPath = $folderPath\n })\n }\n}\nConvertTo-Json -Depth 4 -InputObject @($rows)\n`, 'scan');\n return toArray(output).map(row => {\n const e = record(row);\n return {\n entryId: str(e.entryId),\n storeId: str(e.storeId),\n subject: str(e.subject),\n senderName: str(e.senderName),\n senderEmail: str(e.senderEmail),\n receivedTime: str(e.receivedTime),\n body: base64Text(e.body),\n attachmentNames: toArray(e.attachmentNames).map(str),\n folderPath: str(e.folderPath),\n };\n });\n}\n\n/** The email currently selected — or open — in Outlook. */\nexport async function readSelectedEmail(): Promise<SelectedEmail> {\n const output = await runPowerShellJson(`\n$outlook = New-Object -ComObject Outlook.Application\n$item = $null\ntry {\n $explorer = $outlook.ActiveExplorer()\n if ($explorer -ne $null -and $explorer.Selection.Count -ge 1) { $item = $explorer.Selection.Item(1) }\n} catch {}\nif ($item -eq $null) {\n try {\n $inspector = $outlook.ActiveInspector()\n if ($inspector -ne $null) { $item = $inspector.CurrentItem }\n } catch {}\n}\nif ($item -eq $null) {\n throw \"${failureTag('NOT_FOUND', 'email')}No email is selected in Outlook. Select or open an email, then try again.\"\n}\nif ([string]$item.MessageClass -notlike 'IPM.Note*') {\n throw \"${failureTag('INVALID_REQUEST')}The item selected in Outlook is not an email.\"\n}\n$attachmentNames = @()\ntry { foreach ($att in $item.Attachments) { $attachmentNames += [string]$att.FileName } } catch {}\n$bodyRaw = ''\ntry { $bodyRaw = [string]$item.Body } catch {}\n$storeId = ''\ntry { $storeId = [string]$item.Parent.Store.StoreID } catch {}\n$folderPath = ''\ntry { $folderPath = [string]$item.Parent.FolderPath } catch {}\n${SENDER_SMTP_PS}\n$received = ''\ntry { $received = $item.ReceivedTime.ToString('yyyy-MM-dd HH:mm') } catch {}\nConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{\n entryId = [string]$item.EntryID\n storeId = $storeId\n subject = ([string]$item.Subject).Trim()\n senderName = [string]$item.SenderName\n senderEmail = $senderSmtp\n receivedTime = $received\n body = ${BODY_B64_PS}\n attachmentNames = @($attachmentNames)\n folderPath = $folderPath\n})\n`, 'quick');\n const e = record(output);\n return {\n entryId: str(e.entryId),\n storeId: str(e.storeId),\n subject: str(e.subject),\n senderName: str(e.senderName),\n senderEmail: str(e.senderEmail),\n receivedTime: str(e.receivedTime),\n body: base64Text(e.body),\n attachmentNames: strList(e.attachmentNames),\n folderPath: str(e.folderPath),\n };\n}\n\n/**\n * Open an email in Outlook.\n *\n * Entry ids are rewritten when an item MOVES between folders, so an id recorded\n * before its mail was filed can stop resolving. That fails as NOT_FOUND rather\n * than guessing — the caller is the one who knows what the email was.\n */\nexport async function openOutlookEmail(email: EmailLocator): Promise<void> {\n await runPowerShell(`${SESSION_PS}\n${itemLookupScript(email)}\n$item.Display()\n`, 'quick');\n}\n"]}
|
|
1
|
+
{"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/windows/read.ts"],"names":[],"mappings":"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","sourcesContent":["// Reading mail: one folder, one item, a whole Inbox tree, or whatever is\n// selected in the running Outlook.\nimport { psArray, psBool, psInt, psString, runPowerShell, runPowerShellJson } from './run';\nimport {\n accountScript,\n cutoffScript,\n itemLookupScript,\n itemsSinceScript,\n mailScopeScript,\n NAMED_STORE_PS,\n SENDER_SMTP_PS,\n SESSION_PS,\n stampPs,\n} from './scripts';\nimport { base64Text, record, str, toArray } from './json';\nimport { FolderId, isOutgoingRoot, NON_INCOMING_ROOTS, REPLY_PREFIX_SOURCE, subjectGlobSource } from '../mail';\nimport { failureTag } from '../errors';\nimport type { RawEmail, ReadInboxRequest, SearchRequest } from '../backend';\nimport type { EmailLocator, InboxEmail, InboxSearchMatch, SelectedEmail } from '../types';\n\n/**\n * The COM property carrying each kind of folder's timestamp. Sent Items and\n * Drafts have no ReceivedTime, and filtering them on it returns an empty set\n * that looks exactly like an empty folder.\n */\nfunction dateProperty(rootId: number): string {\n if (rootId === FolderId.Drafts) return 'LastModificationTime';\n return isOutgoingRoot(rootId) ? 'SentOn' : 'ReceivedTime';\n}\n\n/**\n * Bodies cross the PowerShell boundary base64-encoded: they carry quotes,\n * control characters and non-ASCII that would otherwise have to survive JSON\n * escaping in two languages intact.\n */\nconst BODY_B64_PS = `[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes([string]$bodyRaw))`;\n\n/**\n * The item's attachment file names as `$attachmentNames`, unnamed ones\n * included: a name's position is its attachment's. Requires `$item`.\n */\nconst ATTACHMENT_NAMES_PS = `$attachmentNames = @()\ntry { foreach ($att in $item.Attachments) { $attachmentNames += [string]$att.FileName } } catch {}`;\n\n/** Recent mail from the Inbox root or one folder, newest first. */\nexport async function readInboxEmails(request: ReadInboxRequest): Promise<InboxEmail[]> {\n const dateProp = dateProperty(request.folder.rootId);\n const outgoing = isOutgoingRoot(request.folder.rootId);\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${NAMED_STORE_PS}\n${mailScopeScript(request.folder, request.folderLabel)}\n$scopePath = [string]$scope.FolderPath\n${cutoffScript(request.daysBack)}\n${itemsSinceScript('scope', dateProp)}\n$filtered.Sort('[${dateProp}]', $true)\n$limit = ${psInt(request.limit)}\n$previewChars = ${psInt(request.previewChars)}\n$rows = @()\n$count = $filtered.Count\nfor ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {\n $item = $null\n try { $item = $filtered.Item($i) } catch {}\n if ($item -eq $null) { continue }\n $stamp = $null\n try { $stamp = $item.${dateProp} } catch {}\n # Sorted newest first, so the first item older than the window ends it.\n if ($stamp -ne $null -and $stamp -lt $cutoff) { break }\n $preview = ''\n if ($previewChars -gt 0) {\n $bodyText = ''\n try { $bodyText = [string]$item.Body } catch {}\n $preview = if ($bodyText.Length -gt $previewChars) { $bodyText.Substring(0, $previewChars) } else { $bodyText }\n }\n${ATTACHMENT_NAMES_PS}\n $who = ''\n $whoEmail = ''\n if (${psBool(outgoing)}) {\n # Outgoing mail reports who it went TO; \"from\" is always the mailbox itself.\n try { $who = [string]$item.To } catch {}\n $whoEmail = $who\n } else {\n try { $who = [string]$item.SenderName } catch {}\n try { $whoEmail = [string]$item.SenderEmailAddress } catch {}\n }\n $rows += [PSCustomObject]@{\n entryId = [string]$item.EntryID\n storeId = $storeId\n folderPath = $scopePath\n subject = ([string]$item.Subject).Trim()\n senderName = $who\n senderEmail = $whoEmail\n receivedTime = if ($stamp -ne $null) { ${stampPs('$stamp')} } else { '' }\n bodyPreview = $preview\n attachmentNames = @($attachmentNames)\n }\n}\nConvertTo-Json -Depth 4 -InputObject @($rows)\n`, 'standard');\n return toArray(output).map(row => {\n const e = record(row);\n const attachmentNames = toArray(e.attachmentNames).map(str);\n return {\n entryId: str(e.entryId),\n storeId: str(e.storeId),\n subject: str(e.subject),\n senderName: str(e.senderName),\n senderEmail: str(e.senderEmail),\n receivedTime: str(e.receivedTime),\n bodyPreview: str(e.bodyPreview),\n attachmentNames,\n attachmentCount: attachmentNames.length,\n folderPath: str(e.folderPath),\n };\n });\n}\n\n/**\n * One email's full plain-text body, by id.\n *\n * `Body`, not `HTMLBody`: Outlook's own plain-text rendering is what every other\n * reader here consumes, and markup costs an order of magnitude more for the same\n * sentences. The tradeoff is that an HTML table's rows flatten.\n */\nexport async function readEmailBody(email: EmailLocator): Promise<RawEmail> {\n const output = await runPowerShellJson(`${SESSION_PS}\n${itemLookupScript(email)}\n$class = 0\ntry { $class = [int]$item.Class } catch {}\n# 43 = olMail, 46 = olReport (a non-delivery report is still worth reading).\nif ($class -ne 43 -and $class -ne 46) {\n throw \"${failureTag('NOT_FOUND', 'email')}The item with entry id '$lookupId' is not an email (class $class).\"\n}\n$bodyRaw = ''\ntry { $bodyRaw = [string]$item.Body } catch {}\n${ATTACHMENT_NAMES_PS}\n${SENDER_SMTP_PS}\n$received = ''\ntry { $received = ${stampPs('$item.ReceivedTime')} } catch {}\nConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{\n entryId = [string]$item.EntryID\n subject = ([string]$item.Subject).Trim()\n senderName = [string]$item.SenderName\n senderEmail = $senderSmtp\n receivedTime = $received\n body = ${BODY_B64_PS}\n attachmentNames = @($attachmentNames)\n})\n`, 'quick');\n const e = record(output);\n return {\n entryId: str(e.entryId) || email.entryId,\n subject: str(e.subject),\n senderName: str(e.senderName),\n senderEmail: str(e.senderEmail),\n receivedTime: str(e.receivedTime),\n body: base64Text(e.body),\n attachmentNames: toArray(e.attachmentNames).map(str),\n };\n}\n\n/**\n * Walk every folder under the Inbox for the mail matching the request.\n *\n * The window is enforced twice — a date Restrict per folder, then a per-item\n * check — because Restrict is not reliable across stores, and a scan that\n * silently widens on some mailboxes is the failure this guards against.\n * `subjectLike` also drives a server-side prefilter, purely as an optimization:\n * every subject is re-checked, so a store that can't run the query (IMAP stores\n * accept it and match nothing) returns the same set, only slower.\n */\nexport async function searchInboxByFilter(request: SearchRequest): Promise<InboxSearchMatch[]> {\n const subjectFiltered = !!(request.subjectLike || request.subjectPattern || request.excludeReplies);\n // The server-side prefilter speaks DASL, whose `like` wildcards are % and _.\n // A literal % or _ in the glob is left as a wildcard there: that can only\n // widen the prefilter, and the exact test below narrows it again.\n const subjectDasl = request.subjectLike ? request.subjectLike.replace(/\\*/g, '%').replace(/\\?/g, '_') : '';\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${NAMED_STORE_PS}\n$inbox = $store.GetDefaultFolder(${psInt(FolderId.Inbox)})\n$subjectLike = ${psString(request.subjectLike ? subjectGlobSource(request.subjectLike) : '')}\n$subjectDasl = ${psString(subjectDasl)}\n$subjectPattern = ${psString(request.subjectPattern?.source ?? '')}\n$replyPattern = ${psString(request.excludeReplies ? REPLY_PREFIX_SOURCE : '')}\n$requireSubject = ${psBool(subjectFiltered)}\n$requireAttachment = ${psBool(request.requireAttachment)}\n$includeBody = ${psBool(request.includeBody)}\n${cutoffScript(request.daysBack)}\n# Sent, Drafts, Deleted and Junk are siblings of the Inbox on an Exchange profile\n# and CHILDREN of it on an IMAP one. They are cut by id — their names are\n# localized — before the walk, so each takes its whole subtree with it.\n$skipIds = @{}\nforeach ($role in @(${NON_INCOMING_ROOTS.map(psInt).join(', ')})) {\n try {\n $roleFolder = $store.GetDefaultFolder($role)\n if ($roleFolder -ne $null) { $skipIds[[string]$roleFolder.EntryID] = $true }\n } catch {}\n}\n$skipNames = ${psArray(request.excludeFolders)}\n$onlyNames = ${psArray(request.includeFolders)}\nfunction Test-FolderNamed([string]$name, [string]$path, $list) {\n foreach ($entry in $list) {\n if ($name -ieq $entry -or $path -ieq $entry) { return $true }\n }\n return $false\n}\n# Traversal and scanning are separate, and the two lists are not symmetric: an\n# excluded folder is never queued, so its subtree goes with it, while an\n# included one only marks a folder the walk reached anyway — the walk still\n# passes through folders nobody named to reach a nested one somebody did.\n$rootName = ''\ntry { $rootName = [string]$inbox.Name } catch {}\n$rootPath = ''\ntry { $rootPath = [string]$inbox.FolderPath } catch {}\n$folders = [System.Collections.ArrayList]@($inbox)\n$inScope = [System.Collections.ArrayList]@(($onlyNames.Count -eq 0) -or (Test-FolderNamed $rootName $rootPath $onlyNames))\n$walked = 0\nwhile ($walked -lt $folders.Count) {\n try {\n foreach ($sub in $folders[$walked].Folders) {\n if ($skipIds.ContainsKey([string]$sub.EntryID)) { continue }\n $subName = ''\n try { $subName = [string]$sub.Name } catch {}\n $subPath = ''\n try { $subPath = [string]$sub.FolderPath } catch {}\n if (Test-FolderNamed $subName $subPath $skipNames) { continue }\n [void]$folders.Add($sub)\n [void]$inScope.Add(($onlyNames.Count -eq 0) -or (Test-FolderNamed $subName $subPath $onlyNames))\n }\n } catch {}\n $walked++\n}\n# The subject prefilter is DASL rather than Jet: some stores reject Jet's\n# \"[Subject] like\" outright, while DASL is accepted wherever Restrict is. The\n# two syntaxes can't share one filter, so it is a second Restrict on top of the\n# date one. The pattern is concatenated into the query, never interpolated.\n$subjectQuery = '@SQL=' + [char]34 + 'urn:schemas:httpmail:subject' + [char]34 + ' like ' + [char]39 + $subjectDasl.Replace(\"'\", \"''\") + [char]39\n# A list, not an array: += copies the whole array per row, and a wide scan returns thousands.\n$rows = New-Object System.Collections.ArrayList\n$seen = @{}\nfor ($fx = 0; $fx -lt $folders.Count; $fx++) {\n if (-not $inScope[$fx]) { continue }\n $folder = $folders[$fx]\n ${itemsSinceScript('folder', 'ReceivedTime').trim()}\n if ($subjectDasl) {\n try {\n $candidate = $filtered.Restrict($subjectQuery)\n if ($candidate.Count -gt 0) { $filtered = $candidate }\n } catch {}\n }\n $folderPath = ''\n try { $folderPath = [string]$folder.FolderPath } catch {}\n $count = $filtered.Count\n for ($i = 1; $i -le $count; $i++) {\n $item = $null\n try { $item = $filtered.Item($i) } catch {}\n # A restricted collection can hand back nothing for an index, and an item\n # with no id can't be acted on by anything this package does.\n if ($item -eq $null) { continue }\n $entryId = [string]$item.EntryID\n if (-not $entryId -or $seen.ContainsKey($entryId)) { continue }\n $seen[$entryId] = $true\n $subject = ''\n try { $subject = ([string]$item.Subject).Trim() } catch {}\n if ($requireSubject -and -not $subject) { continue }\n if ($subjectLike -and $subject -notmatch $subjectLike) { continue }\n if ($subjectPattern -and $subject -notmatch $subjectPattern) { continue }\n if ($replyPattern -and $subject -imatch $replyPattern) { continue }\n $received = $null\n try { $received = $item.ReceivedTime } catch {}\n if ($received -ne $null -and $received -lt $cutoff) { continue }\n${ATTACHMENT_NAMES_PS}\n if ($requireAttachment -and $attachmentNames.Count -eq 0) { continue }\n $bodyRaw = ''\n if ($includeBody) { try { $bodyRaw = [string]$item.Body } catch {} }\n${SENDER_SMTP_PS}\n [void]$rows.Add([PSCustomObject]@{\n entryId = $entryId\n storeId = $storeId\n subject = $subject\n senderName = [string]$item.SenderName\n senderEmail = $senderSmtp\n receivedTime = if ($received -ne $null) { ${stampPs('$received')} } else { '' }\n body = ${BODY_B64_PS}\n attachmentNames = @($attachmentNames)\n folderPath = $folderPath\n })\n }\n}\nConvertTo-Json -Depth 4 -InputObject @($rows)\n`, 'scan');\n return toArray(output).map(row => {\n const e = record(row);\n return {\n entryId: str(e.entryId),\n storeId: str(e.storeId),\n subject: str(e.subject),\n senderName: str(e.senderName),\n senderEmail: str(e.senderEmail),\n receivedTime: str(e.receivedTime),\n body: base64Text(e.body),\n attachmentNames: toArray(e.attachmentNames).map(str),\n folderPath: str(e.folderPath),\n };\n });\n}\n\n/** The email currently selected — or open — in Outlook. */\nexport async function readSelectedEmail(): Promise<SelectedEmail> {\n const output = await runPowerShellJson(`\n$outlook = New-Object -ComObject Outlook.Application\n$item = $null\ntry {\n $explorer = $outlook.ActiveExplorer()\n if ($explorer -ne $null -and $explorer.Selection.Count -ge 1) { $item = $explorer.Selection.Item(1) }\n} catch {}\nif ($item -eq $null) {\n try {\n $inspector = $outlook.ActiveInspector()\n if ($inspector -ne $null) { $item = $inspector.CurrentItem }\n } catch {}\n}\nif ($item -eq $null) {\n throw \"${failureTag('NOT_FOUND', 'email')}No email is selected in Outlook. Select or open an email, then try again.\"\n}\nif ([string]$item.MessageClass -notlike 'IPM.Note*') {\n throw \"${failureTag('INVALID_REQUEST')}The item selected in Outlook is not an email.\"\n}\n${ATTACHMENT_NAMES_PS}\n$bodyRaw = ''\ntry { $bodyRaw = [string]$item.Body } catch {}\n$storeId = ''\ntry { $storeId = [string]$item.Parent.Store.StoreID } catch {}\n$folderPath = ''\ntry { $folderPath = [string]$item.Parent.FolderPath } catch {}\n${SENDER_SMTP_PS}\n$received = ''\ntry { $received = ${stampPs('$item.ReceivedTime')} } catch {}\nConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{\n entryId = [string]$item.EntryID\n storeId = $storeId\n subject = ([string]$item.Subject).Trim()\n senderName = [string]$item.SenderName\n senderEmail = $senderSmtp\n receivedTime = $received\n body = ${BODY_B64_PS}\n attachmentNames = @($attachmentNames)\n folderPath = $folderPath\n})\n`, 'quick');\n const e = record(output);\n return {\n entryId: str(e.entryId),\n storeId: str(e.storeId),\n subject: str(e.subject),\n senderName: str(e.senderName),\n senderEmail: str(e.senderEmail),\n receivedTime: str(e.receivedTime),\n body: base64Text(e.body),\n // Unnamed attachments keep their place, as in every other reader here.\n attachmentNames: toArray(e.attachmentNames).map(str),\n folderPath: str(e.folderPath),\n };\n}\n\n/**\n * Open an email in Outlook.\n *\n * Entry ids are rewritten when an item MOVES between folders, so an id recorded\n * before its mail was filed can stop resolving. That fails as NOT_FOUND rather\n * than guessing — the caller is the one who knows what the email was.\n */\nexport async function openOutlookEmail(email: EmailLocator): Promise<void> {\n await runPowerShell(`${SESSION_PS}\n${itemLookupScript(email)}\n$item.Display()\n`, 'quick');\n}\n"]}
|
|
@@ -57,6 +57,12 @@ export declare function itemLookupScript(email: EmailLocator): string;
|
|
|
57
57
|
* caller replying to it or matching it against anything needs the address.
|
|
58
58
|
*/
|
|
59
59
|
export declare 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";
|
|
60
|
+
/**
|
|
61
|
+
* A date expression rendered as the timestamp every result carries: local time
|
|
62
|
+
* as 'yyyy-MM-dd HH:mm', which sorts correctly as a plain string and is what
|
|
63
|
+
* macOS's `isoDate` handler produces.
|
|
64
|
+
*/
|
|
65
|
+
export declare function stampPs(dateExpression: string): string;
|
|
60
66
|
/** Recursive, case-insensitive folder search by name, depth-capped so a huge tree can't hang it. */
|
|
61
67
|
export declare 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";
|
|
62
68
|
/**
|
package/dist/windows/scripts.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.FIND_FOLDER_PS = exports.SENDER_SMTP_PS = exports.NAMED_STORE_PS = exports.DELIVERY_STORE_PS = exports.SESSION_PS = void 0;
|
|
4
4
|
exports.accountScript = accountScript;
|
|
5
5
|
exports.itemLookupScript = itemLookupScript;
|
|
6
|
+
exports.stampPs = stampPs;
|
|
6
7
|
exports.cutoffScript = cutoffScript;
|
|
7
8
|
exports.itemsSinceScript = itemsSinceScript;
|
|
8
9
|
exports.mailScopeScript = mailScopeScript;
|
|
@@ -124,6 +125,14 @@ try {
|
|
|
124
125
|
} catch {}
|
|
125
126
|
if (-not $senderSmtp) { try { $senderSmtp = [string]$item.SenderEmailAddress } catch {} }
|
|
126
127
|
`;
|
|
128
|
+
/**
|
|
129
|
+
* A date expression rendered as the timestamp every result carries: local time
|
|
130
|
+
* as 'yyyy-MM-dd HH:mm', which sorts correctly as a plain string and is what
|
|
131
|
+
* macOS's `isoDate` handler produces.
|
|
132
|
+
*/
|
|
133
|
+
function stampPs(dateExpression) {
|
|
134
|
+
return `${dateExpression}.ToString('yyyy-MM-dd HH:mm')`;
|
|
135
|
+
}
|
|
127
136
|
/** Recursive, case-insensitive folder search by name, depth-capped so a huge tree can't hang it. */
|
|
128
137
|
exports.FIND_FOLDER_PS = `
|
|
129
138
|
function Find-FolderByName($root, [string]$name, [int]$depth) {
|
|
@@ -182,7 +191,7 @@ function mailScopeScript(ref, folderLabel, createMissing = false) {
|
|
|
182
191
|
return `
|
|
183
192
|
${exports.FIND_FOLDER_PS}
|
|
184
193
|
$folderLabel = ${(0, run_1.psString)(folderLabel)}
|
|
185
|
-
$segments =
|
|
194
|
+
$segments = ${(0, run_1.psArray)(ref.segments)}
|
|
186
195
|
$scopeRoot = ${root}
|
|
187
196
|
$scope = $scopeRoot
|
|
188
197
|
$deepest = $scopeRoot
|
|
@@ -196,7 +205,7 @@ foreach ($seg in $segments) {
|
|
|
196
205
|
if ($scope -eq $null -and $segments.Count -eq 1) {
|
|
197
206
|
$scope = Find-FolderByName $scopeRoot $segments[0] 3
|
|
198
207
|
}
|
|
199
|
-
if ($scope -eq $null -and ${createMissing
|
|
208
|
+
if ($scope -eq $null -and ${(0, run_1.psBool)(createMissing)}) {
|
|
200
209
|
# Rebuild the chain from the root, creating only what is genuinely absent.
|
|
201
210
|
$scope = $scopeRoot
|
|
202
211
|
foreach ($seg in $segments) {
|
|
@@ -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;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"]}
|
|
1
|
+
{"version":3,"file":"scripts.js","sourceRoot":"","sources":["../../src/windows/scripts.ts"],"names":[],"mappings":";;;AAuCA,sCAoBC;AAsCD,4CAaC;AAqBD,0BAEC;AA2BD,oCAKC;AAOD,4CAKC;AAYD,0CAmCC;AAhOD,0EAA0E;AAC1E,EAAE;AACF,uEAAuE;AACvE,+EAA+E;AAC/E,8EAA8E;AAC9E,2CAA2C;AAC3C,+BAAyD;AACzD,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;;;;GAIG;AACH,SAAgB,OAAO,CAAC,cAAsB;IAC1C,OAAO,GAAG,cAAc,+BAA+B,CAAC;AAC5D,CAAC;AAED,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;cACxB,IAAA,aAAO,EAAC,GAAG,CAAC,QAAQ,CAAC;eACpB,IAAI;;;;;;;;;;;;;4BAaS,IAAA,YAAM,EAAC,aAAa,CAAC;;;;;;;;;;;;;aAapC,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 { psArray, psBool, 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/**\n * A date expression rendered as the timestamp every result carries: local time\n * as 'yyyy-MM-dd HH:mm', which sorts correctly as a plain string and is what\n * macOS's `isoDate` handler produces.\n */\nexport function stampPs(dateExpression: string): string {\n return `${dateExpression}.ToString('yyyy-MM-dd HH:mm')`;\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 = ${psArray(ref.segments)}\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 ${psBool(createMissing)}) {\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
|
@@ -7,5 +7,9 @@ import type { ReplyEmailResult, SendEmailResult } from '../types';
|
|
|
7
7
|
* draft then keeps, and the one listOutlookDrafts reports.
|
|
8
8
|
*/
|
|
9
9
|
export declare function sendOutlookEmail(request: SendRequest): Promise<SendEmailResult>;
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Reply to an email, with the composed HTML above the quoted original. A reply
|
|
12
|
+
* filed as a draft reports its EntryID, read after `Save()` assigns it, as a
|
|
13
|
+
* new email's does.
|
|
14
|
+
*/
|
|
11
15
|
export declare function replyOutlookEmail(request: ReplyRequest): Promise<ReplyEmailResult>;
|
package/dist/windows/send.js
CHANGED
|
@@ -7,7 +7,7 @@ const run_1 = require("./run");
|
|
|
7
7
|
const scripts_1 = require("./scripts");
|
|
8
8
|
const replyBody_1 = require("../shared/replyBody");
|
|
9
9
|
const errors_1 = require("../errors");
|
|
10
|
-
const json_1 = require("
|
|
10
|
+
const json_1 = require("./json");
|
|
11
11
|
/**
|
|
12
12
|
* Set the sending account through IDispatch reflection.
|
|
13
13
|
*
|
|
@@ -78,9 +78,14 @@ ConvertTo-Json -Compress @{ draftEntryId = ${draftEntryId} }
|
|
|
78
78
|
body.cleanup();
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Reply to an email, with the composed HTML above the quoted original. A reply
|
|
83
|
+
* filed as a draft reports its EntryID, read after `Save()` assigns it, as a
|
|
84
|
+
* new email's does.
|
|
85
|
+
*/
|
|
82
86
|
async function replyOutlookEmail(request) {
|
|
83
87
|
const insert = (0, run_1.scriptInput)('reply-insert', request.html);
|
|
88
|
+
const draftEntryId = request.disposition === 'save' ? '[string]$reply.EntryID' : "''";
|
|
84
89
|
try {
|
|
85
90
|
const output = await (0, run_1.runPowerShellJson)(`${(0, scripts_1.accountScript)(request.account)}
|
|
86
91
|
${(0, scripts_1.itemLookupScript)(request.email)}
|
|
@@ -101,10 +106,15 @@ $to = [string]$reply.To
|
|
|
101
106
|
$subject = [string]$reply.Subject
|
|
102
107
|
${scripts_1.SENDER_SMTP_PS}
|
|
103
108
|
${dispose('reply', request.disposition)}
|
|
104
|
-
ConvertTo-Json -Compress @{ to = $to; subject = $subject; repliedToSender = $senderSmtp }
|
|
109
|
+
ConvertTo-Json -Compress @{ to = $to; subject = $subject; repliedToSender = $senderSmtp; draftEntryId = ${draftEntryId} }
|
|
105
110
|
`, 'standard');
|
|
106
111
|
const result = (0, json_1.record)(output);
|
|
107
|
-
return {
|
|
112
|
+
return {
|
|
113
|
+
to: (0, json_1.str)(result.to),
|
|
114
|
+
subject: (0, json_1.str)(result.subject),
|
|
115
|
+
repliedToSender: (0, json_1.str)(result.repliedToSender),
|
|
116
|
+
draftEntryId: (0, json_1.str)(result.draftEntryId),
|
|
117
|
+
};
|
|
108
118
|
}
|
|
109
119
|
finally {
|
|
110
120
|
insert.cleanup();
|
package/dist/windows/send.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"send.js","sourceRoot":"","sources":["../../src/windows/send.ts"],"names":[],"mappings":";;AAuDA,4CAyBC;
|
|
1
|
+
{"version":3,"file":"send.js","sourceRoot":"","sources":["../../src/windows/send.ts"],"names":[],"mappings":"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","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 './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/**\n * Reply to an email, with the composed HTML above the quoted original. A reply\n * filed as a draft reports its EntryID, read after `Save()` assigns it, as a\n * new email's does.\n */\nexport async function replyOutlookEmail(request: ReplyRequest): Promise<ReplyEmailResult> {\n const insert = scriptInput('reply-insert', request.html);\n const draftEntryId = request.disposition === 'save' ? '[string]$reply.EntryID' : \"''\";\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; draftEntryId = ${draftEntryId} }\n`, 'standard');\n const result = record(output);\n return {\n to: str(result.to),\n subject: str(result.subject),\n repliedToSender: str(result.repliedToSender),\n draftEntryId: str(result.draftEntryId),\n };\n } finally {\n insert.cleanup();\n }\n}\n"]}
|
|
@@ -7,7 +7,7 @@ exports.editEmailTemplate = editEmailTemplate;
|
|
|
7
7
|
// email bodies, plus the compose-window editor for one.
|
|
8
8
|
const run_1 = require("./run");
|
|
9
9
|
const scripts_1 = require("./scripts");
|
|
10
|
-
const json_1 = require("
|
|
10
|
+
const json_1 = require("./json");
|
|
11
11
|
const templateLimits_1 = require("../shared/templateLimits");
|
|
12
12
|
const templateBody_1 = require("../templateBody");
|
|
13
13
|
const mail_1 = require("../mail");
|
|
@@ -77,7 +77,7 @@ for ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {
|
|
|
77
77
|
if ($preview.Length -gt $previewChars) { $preview = $preview.Substring(0, $previewChars) }
|
|
78
78
|
}
|
|
79
79
|
$modified = ''
|
|
80
|
-
try { $modified = $item.LastModificationTime
|
|
80
|
+
try { $modified = ${(0, scripts_1.stampPs)('$item.LastModificationTime')} } catch {}
|
|
81
81
|
$rows += [PSCustomObject]@{
|
|
82
82
|
entryId = [string]$item.EntryID
|
|
83
83
|
subject = $subject
|
|
@@ -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 {\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"]}
|
|
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, stampPs } from './scripts';\nimport { bool, record, str, toArray } from './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 = ${stampPs('$item.LastModificationTime')} } 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.2.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",
|
package/dist/shared/json.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"json.js","sourceRoot":"","sources":["../../src/shared/json.ts"],"names":[],"mappings":";;AASA,0BAEC;AAGD,wBAIC;AAED,kBAEC;AAED,kBAEC;AAED,oBAEC;AAGD,0BAEC;AAGD,gCAEC;AASD,sCAEC;AAGD,oCAKC;AAnDD,iGAAiG;AACjG,SAAgB,OAAO,CAAC,KAAc;IAClC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AACvE,CAAC;AAED,iEAAiE;AACjE,SAAgB,MAAM,CAAC,KAAc;IACjC,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACvE,CAAC,CAAC,KAAgC;QAClC,CAAC,CAAC,EAAE,CAAC;AACb,CAAC;AAED,SAAgB,GAAG,CAAC,KAAc;IAC9B,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC9C,CAAC;AAED,SAAgB,GAAG,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC3E,CAAC;AAED,SAAgB,IAAI,CAAC,KAAc;IAC/B,OAAO,KAAK,KAAK,IAAI,CAAC;AAC1B,CAAC;AAED,mCAAmC;AACnC,SAAgB,OAAO,CAAC,KAAc;IAClC,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AACnD,CAAC;AAED,0FAA0F;AAC1F,SAAgB,UAAU,CAAC,KAAc;IACrC,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,aAAa,CAAC,UAA8B;IACxD,OAAO,2CAA2C,UAAU,sDAAsD,CAAC;AACvH,CAAC;AAED,mEAAmE;AACnE,SAAgB,YAAY,CAAC,KAAc;IACvC,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;QAC5B,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QACtB,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;IACrF,CAAC,CAAC,CAAC;AACP,CAAC","sourcesContent":["// Coercing the JSON a PowerShell script prints into the package's types.\n//\n// ConvertTo-Json has two habits a caller must never see: it unwraps a\n// single-element array into a bare object, and it renders an empty one as\n// nothing at all. Every list crossing that boundary goes through `toArray`, and\n// every scalar through a coercion that survives a missing property.\nimport type { ItemFailure } from '../types';\n\n/** One value as a list: an array stays, null and undefined become empty, anything else wraps. */\nexport function toArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : value == null ? [] : [value];\n}\n\n/** A value as a plain object, for reading fields off one row. */\nexport function record(value: unknown): Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n ? value as Record<string, unknown>\n : {};\n}\n\nexport function str(value: unknown): string {\n return value == null ? '' : String(value);\n}\n\nexport function num(value: unknown): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0;\n}\n\nexport function bool(value: unknown): boolean {\n return value === true;\n}\n\n/** A list of non-empty strings. */\nexport function strList(value: unknown): string[] {\n return toArray(value).map(str).filter(Boolean);\n}\n\n/** A base64 field decoded as UTF-8 — how scripts pass bodies that must survive intact. */\nexport function base64Text(value: unknown): string {\n return Buffer.from(str(value), 'base64').toString('utf8');\n}\n\n/**\n * The PowerShell that appends one item's failure to `$failed`, inside the\n * `catch` of a per-item loop: the row `itemFailures` reads back, written once\n * so no script can name its fields differently. `idVariable` holds the item's\n * id. `$subject` must be reset before each item's `try`, or a failure reports\n * the previous item's subject.\n */\nexport function itemFailurePs(idVariable: '$id' | '$entryId'): string {\n return `$failed += [PSCustomObject]@{ entryId = ${idVariable}; subject = $subject; error = $_.Exception.Message }`;\n}\n\n/** A list of per-item failures, as `itemFailurePs` writes them. */\nexport function itemFailures(value: unknown): ItemFailure[] {\n return toArray(value).map(row => {\n const e = record(row);\n return { entryId: str(e.entryId), subject: str(e.subject), error: str(e.error) };\n });\n}\n"]}
|
|
File without changes
|
|
File without changes
|