@dawnlit/outlook-bridge 2.0.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 +16 -9
- package/dist/backend.d.ts +2 -2
- package/dist/backend.js.map +1 -1
- package/dist/bridge.js +3 -3
- package/dist/bridge.js.map +1 -1
- package/dist/errors.d.ts +17 -8
- package/dist/errors.js +17 -13
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +3 -3
- 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/attachments.js +1 -1
- package/dist/mac/attachments.js.map +1 -1
- package/dist/mac/bounces.js +18 -31
- package/dist/mac/bounces.js.map +1 -1
- package/dist/mac/cleanup.d.ts +11 -3
- package/dist/mac/cleanup.js +30 -58
- package/dist/mac/cleanup.js.map +1 -1
- package/dist/mac/drafts.d.ts +3 -1
- package/dist/mac/drafts.js +37 -49
- package/dist/mac/drafts.js.map +1 -1
- package/dist/mac/folders.d.ts +10 -3
- package/dist/mac/folders.js +31 -23
- 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.d.ts +4 -0
- package/dist/mac/read.js +68 -42
- package/dist/mac/read.js.map +1 -1
- package/dist/mac/run.d.ts +19 -5
- package/dist/mac/run.js +49 -9
- package/dist/mac/run.js.map +1 -1
- package/dist/mac/scripts.d.ts +67 -14
- package/dist/mac/scripts.js +150 -31
- package/dist/mac/scripts.js.map +1 -1
- package/dist/mac/send.d.ts +5 -4
- package/dist/mac/send.js +52 -14
- package/dist/mac/send.js.map +1 -1
- package/dist/mac/templates.js +16 -22
- package/dist/mac/templates.js.map +1 -1
- package/dist/mail.d.ts +11 -3
- package/dist/mail.js +25 -6
- package/dist/mail.js.map +1 -1
- package/dist/runtime.js.map +1 -1
- package/dist/shared/args.js.map +1 -1
- package/dist/shared/attachmentMatch.d.ts +0 -6
- package/dist/shared/attachmentMatch.js +0 -1
- package/dist/shared/attachmentMatch.js.map +1 -1
- package/dist/shared/exec.js.map +1 -1
- package/dist/shared/locate.d.ts +0 -8
- package/dist/shared/locate.js +0 -1
- package/dist/shared/locate.js.map +1 -1
- package/dist/shared/replyBody.d.ts +0 -15
- package/dist/shared/replyBody.js +0 -2
- package/dist/shared/replyBody.js.map +1 -1
- package/dist/shared/templateLimits.d.ts +14 -0
- package/dist/shared/templateLimits.js +21 -0
- package/dist/shared/templateLimits.js.map +1 -0
- package/dist/templateBody.d.ts +6 -0
- package/dist/templateBody.js +8 -1
- package/dist/templateBody.js.map +1 -1
- package/dist/types.d.ts +43 -7
- 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 +7 -7
- package/dist/windows/bounces.js.map +1 -1
- package/dist/windows/cleanup.d.ts +5 -1
- package/dist/windows/cleanup.js +18 -15
- package/dist/windows/cleanup.js.map +1 -1
- package/dist/windows/drafts.d.ts +4 -4
- package/dist/windows/drafts.js +22 -33
- package/dist/windows/drafts.js.map +1 -1
- package/dist/windows/folders.js +2 -2
- package/dist/windows/folders.js.map +1 -1
- package/dist/{shared → windows}/json.d.ts +11 -6
- package/dist/{shared → windows}/json.js +12 -7
- package/dist/windows/json.js.map +1 -0
- package/dist/windows/read.js +23 -16
- package/dist/windows/read.js.map +1 -1
- package/dist/windows/run.d.ts +0 -2
- package/dist/windows/run.js +1 -2
- package/dist/windows/run.js.map +1 -1
- package/dist/windows/scripts.d.ts +10 -4
- package/dist/windows/scripts.js +21 -12
- package/dist/windows/scripts.js.map +1 -1
- package/dist/windows/send.d.ts +13 -4
- package/dist/windows/send.js +24 -6
- package/dist/windows/send.js.map +1 -1
- package/dist/windows/templates.js +11 -17
- package/dist/windows/templates.js.map +1 -1
- package/package.json +2 -2
- package/dist/shared/json.js.map +0 -1
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, 'filtered')}\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', 'filtered').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${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})\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 };\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"]}
|
package/dist/windows/run.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { type RunBudget } from '../runtime';
|
|
2
|
-
/** Refuse before generating anything when this isn't Windows. */
|
|
3
|
-
export declare function requireWindows(): void;
|
|
4
2
|
/** Escape text for the inside of a PowerShell single-quoted literal. */
|
|
5
3
|
export declare function psEscape(text: string): string;
|
|
6
4
|
/** Caller text as a PowerShell single-quoted literal. */
|
package/dist/windows/run.js
CHANGED
|
@@ -3,7 +3,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.requireWindows = requireWindows;
|
|
7
6
|
exports.psEscape = psEscape;
|
|
8
7
|
exports.psString = psString;
|
|
9
8
|
exports.psArray = psArray;
|
|
@@ -27,7 +26,7 @@ const exec_1 = require("../shared/exec");
|
|
|
27
26
|
const RUNNER = 'powershell';
|
|
28
27
|
/** Opens the line a script's catch block writes its failure on. */
|
|
29
28
|
const FAILURE_MARKER = '@@outlook-bridge-failure@@';
|
|
30
|
-
/** Refuse before
|
|
29
|
+
/** Refuse before writing or running a script when this isn't Windows. */
|
|
31
30
|
function requireWindows() {
|
|
32
31
|
if (process.platform !== 'win32')
|
|
33
32
|
throw new errors_1.UnsupportedPlatformError(process.platform);
|
package/dist/windows/run.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/windows/run.ts"],"names":[],"mappings":"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","sourcesContent":["// Getting a generated script to Windows PowerShell and its output back.\n//\n// Every Windows operation is \"build a script, run it, read what it printed\",\n// so this module owns the parts nobody should reimplement: how a script is\n// wrapped and run, how a failure is read back out of it, and the escaping rules\n// for putting caller text into a script safely.\nimport fs from 'fs';\nimport { InvalidRequestError, ScriptError, UnsupportedPlatformError } from '../errors';\nimport { removeTempFile, type RunBudget, tempFile } from '../runtime';\nimport { runScriptProcess } from '../shared/exec';\n\nconst RUNNER = 'powershell' as const;\n\n/** Opens the line a script's catch block writes its failure on. */\nconst FAILURE_MARKER = '@@outlook-bridge-failure@@';\n\n/** Refuse before generating anything when this isn't Windows. */\nexport function requireWindows(): void {\n if (process.platform !== 'win32') throw new UnsupportedPlatformError(process.platform);\n}\n\n// ── Escaping ─────────────────────────────────────────────────────────────\n//\n// Caller text goes into a script ONLY as a single-quoted literal, built by\n// `psString`. Two things make that the rule:\n// - A double-quoted PowerShell string expands `$(...)`, so caller text placed\n// in one runs as code. Where a message needs caller text, it is assigned to a\n// variable first and the double-quoted string names the variable — a\n// variable's value is never evaluated again.\n// - PowerShell accepts the typographic quotes ‘ ’ ‚ ‛ as single quotes too, so\n// doubling only the ASCII one leaves a way out of the literal. Every quote\n// character is doubled, which PowerShell reads back as that same character.\n\n/** Escape text for the inside of a PowerShell single-quoted literal. */\nexport function psEscape(text: string): string {\n return text.replace(/['\\u2018\\u2019\\u201A\\u201B]/g, quote => quote + quote);\n}\n\n/** Caller text as a PowerShell single-quoted literal. */\nexport function psString(text: string): string {\n return `'${psEscape(text)}'`;\n}\n\n/** Strings as a PowerShell array expression: `@('a', 'b')`. */\nexport function psArray(values: readonly string[]): string {\n return `@(${values.map(psString).join(', ')})`;\n}\n\n/** `$true` / `$false` — PowerShell has no bare boolean literal. */\nexport function psBool(value: boolean): string {\n return value ? '$true' : '$false';\n}\n\n/**\n * A whole number for a script. The operations layer has already validated every\n * count, so this refusing anything else is the second line of defence: a\n * non-number never reaches a script as code.\n */\nexport function psInt(value: number): string {\n if (!Number.isSafeInteger(value)) {\n throw new InvalidRequestError(`Expected a whole number, got ${String(value)}.`);\n }\n return String(value);\n}\n\n// ── Running ──────────────────────────────────────────────────────────────\n\n/**\n * The complete script for a body: UTF-8 output, stop-on-error, and a catch that\n * reports any failure as one JSON line on stderr.\n *\n * UTF-8 output matters because Windows PowerShell 5.1 otherwise writes in the\n * console's legacy code page, whose \"best fit\" mapping rewrites non-ASCII — a\n * curly quote in a subject becomes a straight one — which corrupts the JSON\n * the scripts print. The catch matters because an uncaught PowerShell error\n * prints a multi-line error record (\"At line:12 char:48 …\") that would\n * otherwise become the error's message.\n */\nexport function buildScript(body: string): string {\n return `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\n$ErrorActionPreference = 'Stop'\n$WarningPreference = 'SilentlyContinue'\n$ProgressPreference = 'SilentlyContinue'\ntry {\n${body}\n} catch {\n $failure = $_\n $message = [string]$failure.Exception.Message\n if (-not $message) { $message = [string]$failure }\n $line = 0\n try { $line = [int]$failure.InvocationInfo.ScriptLineNumber } catch {}\n [Console]::Error.WriteLine('${FAILURE_MARKER}' + (ConvertTo-Json -Compress @{ message = $message; line = $line }))\n exit 1\n}\n`;\n}\n\n/** The failure a script's catch block reported, or its raw stderr when it never got that far. */\nfunction parseFailure(stderr: string): { message: string; line?: number } {\n const at = stderr.lastIndexOf(FAILURE_MARKER);\n if (at !== -1) {\n const json = stderr.slice(at + FAILURE_MARKER.length).split(/\\r?\\n/)[0];\n try {\n const parsed = JSON.parse(json) as { message?: unknown; line?: unknown };\n const line = typeof parsed.line === 'number' && parsed.line > 0 ? parsed.line : undefined;\n return {message: String(parsed.message ?? ''), line};\n } catch {\n // Fall through to the raw stderr.\n }\n }\n // A script that fails to PARSE never enters its try block, so PowerShell's\n // own report is all there is.\n return {message: stderr.trim()};\n}\n\nasync function execute(body: string, budget: RunBudget): Promise<{\n stdout: string;\n script: string;\n durationMs: number\n}> {\n requireWindows();\n const script = buildScript(body);\n // Always from a file, never `-Command`: a file has no command-line length\n // limit and no second layer of quoting. The byte-order mark is what makes\n // PowerShell 5.1 read it as UTF-8 rather than the ANSI code page, which\n // would mangle every non-ASCII character in a subject, name or path.\n const file = tempFile('script', 'ps1');\n fs.writeFileSync(file, '\\uFEFF' + script, 'utf8');\n try {\n const {stdout, durationMs} = await runScriptProcess({\n runner: RUNNER,\n command: 'powershell.exe',\n args: ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', file],\n script,\n budget,\n parseFailure,\n });\n return {stdout, script, durationMs};\n } finally {\n removeTempFile(file);\n }\n}\n\n/** Run a script body and return what it printed, trimmed. */\nexport async function runPowerShell(body: string, budget: RunBudget): Promise<string> {\n return (await execute(body, budget)).stdout.trim();\n}\n\n/**\n * Run a script body that prints JSON, and return it parsed — null when the\n * script printed nothing. Output that isn't JSON is a SCRIPT_FAILED carrying\n * the script, never a bare SyntaxError.\n */\nexport async function runPowerShellJson(body: string, budget: RunBudget): Promise<unknown> {\n const {stdout, script, durationMs} = await execute(body, budget);\n const output = stdout.trim();\n if (output === '' || output === 'null') return null;\n try {\n return JSON.parse(output);\n } catch (error) {\n throw new ScriptError({\n runner: RUNNER,\n script,\n stderr: '',\n durationMs,\n message: `PowerShell printed output that is not valid JSON: ${output.slice(0, 200)}`,\n cause: error,\n });\n }\n}\n\n/**\n * Write text a script will read back, and hand back its cleanup.\n *\n * Large HTML — an email body, a Word-sized template — goes through a file rather\n * than the script itself, where it would have to be escaped and would bloat\n * every error and debug event that carries the script.\n */\nexport function scriptInput(kind: string, contents: string): { path: string; cleanup(): void } {\n const file = tempFile(kind, 'html');\n fs.writeFileSync(file, contents, 'utf8');\n return {path: file, cleanup: () => removeTempFile(file)};\n}\n\n/**\n * Read a file a script wrote, as UTF-8, and delete it; '' when it never\n * appeared. .NET's UTF-8 writer opens the file with a byte-order mark, which is\n * not part of the text.\n */\nexport function readScriptOutput(file: string): string {\n try {\n return fs.readFileSync(file, 'utf8').replace(/^\\uFEFF/, '');\n } catch {\n return '';\n } finally {\n removeTempFile(file);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/windows/run.ts"],"names":[],"mappings":"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","sourcesContent":["// Getting a generated script to Windows PowerShell and its output back.\n//\n// Every Windows operation is \"build a script, run it, read what it printed\",\n// so this module owns the parts nobody should reimplement: how a script is\n// wrapped and run, how a failure is read back out of it, and the escaping rules\n// for putting caller text into a script safely.\nimport fs from 'fs';\nimport { InvalidRequestError, ScriptError, UnsupportedPlatformError } from '../errors';\nimport { removeTempFile, type RunBudget, tempFile } from '../runtime';\nimport { type ProcessOutput, runScriptProcess } from '../shared/exec';\n\nconst RUNNER = 'powershell' as const;\n\n/** Opens the line a script's catch block writes its failure on. */\nconst FAILURE_MARKER = '@@outlook-bridge-failure@@';\n\n/** Refuse before writing or running a script when this isn't Windows. */\nfunction requireWindows(): void {\n if (process.platform !== 'win32') throw new UnsupportedPlatformError(process.platform);\n}\n\n// ── Escaping ─────────────────────────────────────────────────────────────\n//\n// Caller text goes into a script ONLY as a single-quoted literal, built by\n// `psString`. Two things make that the rule:\n// - A double-quoted PowerShell string expands `$(...)`, so caller text placed\n// in one runs as code. Where a message needs caller text, it is assigned to a\n// variable first and the double-quoted string names the variable — a\n// variable's value is never evaluated again.\n// - PowerShell accepts the typographic quotes ‘ ’ ‚ ‛ as single quotes too, so\n// doubling only the ASCII one leaves a way out of the literal. Every quote\n// character is doubled, which PowerShell reads back as that same character.\n\n/** Escape text for the inside of a PowerShell single-quoted literal. */\nexport function psEscape(text: string): string {\n return text.replace(/['\\u2018\\u2019\\u201A\\u201B]/g, quote => quote + quote);\n}\n\n/** Caller text as a PowerShell single-quoted literal. */\nexport function psString(text: string): string {\n return `'${psEscape(text)}'`;\n}\n\n/** Strings as a PowerShell array expression: `@('a', 'b')`. */\nexport function psArray(values: readonly string[]): string {\n return `@(${values.map(psString).join(', ')})`;\n}\n\n/** `$true` / `$false` — PowerShell has no bare boolean literal. */\nexport function psBool(value: boolean): string {\n return value ? '$true' : '$false';\n}\n\n/**\n * A whole number for a script. The operations layer has already validated every\n * count, so this refusing anything else is the second line of defence: a\n * non-number never reaches a script as code.\n */\nexport function psInt(value: number): string {\n if (!Number.isSafeInteger(value)) {\n throw new InvalidRequestError(`Expected a whole number, got ${String(value)}.`);\n }\n return String(value);\n}\n\n// ── Running ──────────────────────────────────────────────────────────────\n\n/**\n * The complete script for a body: UTF-8 output, stop-on-error, and a catch that\n * reports any failure as one JSON line on stderr.\n *\n * UTF-8 output matters because Windows PowerShell 5.1 otherwise writes in the\n * console's legacy code page, whose \"best fit\" mapping rewrites non-ASCII — a\n * curly quote in a subject becomes a straight one — which corrupts the JSON\n * the scripts print. The catch matters because an uncaught PowerShell error\n * prints a multi-line error record (\"At line:12 char:48 …\") that would\n * otherwise become the error's message.\n */\nexport function buildScript(body: string): string {\n return `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\n$ErrorActionPreference = 'Stop'\n$WarningPreference = 'SilentlyContinue'\n$ProgressPreference = 'SilentlyContinue'\ntry {\n${body}\n} catch {\n $failure = $_\n $message = [string]$failure.Exception.Message\n if (-not $message) { $message = [string]$failure }\n $line = 0\n try { $line = [int]$failure.InvocationInfo.ScriptLineNumber } catch {}\n [Console]::Error.WriteLine('${FAILURE_MARKER}' + (ConvertTo-Json -Compress @{ message = $message; line = $line }))\n exit 1\n}\n`;\n}\n\n/** The failure a script's catch block reported, or its raw stderr when it never got that far. */\nfunction parseFailure(stderr: string): { message: string; line?: number } {\n const at = stderr.lastIndexOf(FAILURE_MARKER);\n if (at !== -1) {\n const json = stderr.slice(at + FAILURE_MARKER.length).split(/\\r?\\n/)[0];\n try {\n const parsed = JSON.parse(json) as { message?: unknown; line?: unknown };\n const line = typeof parsed.line === 'number' && parsed.line > 0 ? parsed.line : undefined;\n return { message: String(parsed.message ?? ''), line };\n } catch {\n // Fall through to the raw stderr.\n }\n }\n // A script that fails to PARSE never enters its try block, so PowerShell's\n // own report is all there is.\n return { message: stderr.trim() };\n}\n\nasync function execute(body: string, budget: RunBudget): Promise<ProcessOutput & { script: string }> {\n requireWindows();\n const script = buildScript(body);\n // Always from a file, never `-Command`: a file has no command-line length\n // limit and no second layer of quoting. The byte-order mark is what makes\n // PowerShell 5.1 read it as UTF-8 rather than the ANSI code page, which\n // would mangle every non-ASCII character in a subject, name or path.\n const file = tempFile('script', 'ps1');\n fs.writeFileSync(file, '\\uFEFF' + script, 'utf8');\n try {\n const { stdout, durationMs } = await runScriptProcess({\n runner: RUNNER,\n command: 'powershell.exe',\n args: ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', file],\n script,\n budget,\n parseFailure,\n });\n return { stdout, script, durationMs };\n } finally {\n removeTempFile(file);\n }\n}\n\n/** Run a script body and return what it printed, trimmed. */\nexport async function runPowerShell(body: string, budget: RunBudget): Promise<string> {\n return (await execute(body, budget)).stdout.trim();\n}\n\n/**\n * Run a script body that prints JSON, and return it parsed — null when the\n * script printed nothing. Output that isn't JSON is a SCRIPT_FAILED carrying\n * the script, never a bare SyntaxError.\n */\nexport async function runPowerShellJson(body: string, budget: RunBudget): Promise<unknown> {\n const { stdout, script, durationMs } = await execute(body, budget);\n const output = stdout.trim();\n if (output === '' || output === 'null') return null;\n try {\n return JSON.parse(output);\n } catch (error) {\n throw new ScriptError({\n runner: RUNNER,\n script,\n stderr: '',\n durationMs,\n message: `PowerShell printed output that is not valid JSON: ${output.slice(0, 200)}`,\n cause: error,\n });\n }\n}\n\n/**\n * Write text a script will read back, and hand back its cleanup.\n *\n * Large HTML — an email body, a Word-sized template — goes through a file rather\n * than the script itself, where it would have to be escaped and would bloat\n * every error and debug event that carries the script.\n */\nexport function scriptInput(kind: string, contents: string): { path: string; cleanup(): void } {\n const file = tempFile(kind, 'html');\n fs.writeFileSync(file, contents, 'utf8');\n return { path: file, cleanup: () => removeTempFile(file) };\n}\n\n/**\n * Read a file a script wrote, as UTF-8, and delete it; '' when it never\n * appeared. .NET's UTF-8 writer opens the file with a byte-order mark, which is\n * not part of the text.\n */\nexport function readScriptOutput(file: string): string {\n try {\n return fs.readFileSync(file, 'utf8').replace(/^\\uFEFF/, '');\n } catch {\n return '';\n } finally {\n removeTempFile(file);\n }\n}\n"]}
|
|
@@ -43,13 +43,13 @@ export declare const DELIVERY_STORE_PS = "\n$store = if ($account -ne $null) { $
|
|
|
43
43
|
*/
|
|
44
44
|
export declare const NAMED_STORE_PS: string;
|
|
45
45
|
/**
|
|
46
|
-
* Resolve one email into
|
|
46
|
+
* Resolve one email into `$item`, or fail as NOT_FOUND.
|
|
47
47
|
*
|
|
48
48
|
* The StoreID is tried first, since it lets an id from any mounted mailbox
|
|
49
49
|
* resolve; the bare lookup (default store) is the fallback for a caller holding
|
|
50
50
|
* a stale StoreID or none.
|
|
51
51
|
*/
|
|
52
|
-
export declare function itemLookupScript(email: EmailLocator
|
|
52
|
+
export declare function itemLookupScript(email: EmailLocator): string;
|
|
53
53
|
/**
|
|
54
54
|
* The sender's SMTP address as `$senderSmtp`. Requires `$item`.
|
|
55
55
|
*
|
|
@@ -57,6 +57,12 @@ export declare function itemLookupScript(email: EmailLocator, variable?: 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
|
/**
|
|
@@ -71,11 +77,11 @@ export declare const FIND_FOLDER_PS = "\nfunction Find-FolderByName($root, [stri
|
|
|
71
77
|
*/
|
|
72
78
|
export declare function cutoffScript(daysBack: number): string;
|
|
73
79
|
/**
|
|
74
|
-
*
|
|
80
|
+
* `$filtered` = the items of `$<folderVariable>` with `property` on or after
|
|
75
81
|
* `$cutoff`. Requires `cutoffScript`. A store that rejects the filter outright
|
|
76
82
|
* yields every item instead, which the per-item re-check then narrows.
|
|
77
83
|
*/
|
|
78
|
-
export declare function itemsSinceScript(folderVariable: string, property: string
|
|
84
|
+
export declare function itemsSinceScript(folderVariable: string, property: string): string;
|
|
79
85
|
/**
|
|
80
86
|
* Resolve `$scope` from `$store`: a well-known root walked down through any
|
|
81
87
|
* further segments. Also binds `$scopeCreated`.
|
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;
|
|
@@ -91,22 +92,22 @@ $store = $storeFolder.Store
|
|
|
91
92
|
$storeId = $store.StoreID
|
|
92
93
|
`;
|
|
93
94
|
/**
|
|
94
|
-
* Resolve one email into
|
|
95
|
+
* Resolve one email into `$item`, or fail as NOT_FOUND.
|
|
95
96
|
*
|
|
96
97
|
* The StoreID is tried first, since it lets an id from any mounted mailbox
|
|
97
98
|
* resolve; the bare lookup (default store) is the fallback for a caller holding
|
|
98
99
|
* a stale StoreID or none.
|
|
99
100
|
*/
|
|
100
|
-
function itemLookupScript(email
|
|
101
|
+
function itemLookupScript(email) {
|
|
101
102
|
const byStore = email.storeId
|
|
102
|
-
? `try {
|
|
103
|
+
? `try { $item = $ns.GetItemFromID($lookupId, ${(0, run_1.psString)(email.storeId)}) } catch { $item = $null }`
|
|
103
104
|
: '';
|
|
104
105
|
return `
|
|
105
106
|
$lookupId = ${(0, run_1.psString)(email.entryId)}
|
|
106
|
-
|
|
107
|
+
$item = $null
|
|
107
108
|
${byStore}
|
|
108
|
-
if (
|
|
109
|
-
if (
|
|
109
|
+
if ($item -eq $null) { try { $item = $ns.GetItemFromID($lookupId) } catch { $item = $null } }
|
|
110
|
+
if ($item -eq $null) {
|
|
110
111
|
throw "${(0, errors_1.failureTag)('NOT_FOUND', 'email')}No email found for entry id '$lookupId'. It may have been moved or deleted - list the mail again for a current id."
|
|
111
112
|
}
|
|
112
113
|
`;
|
|
@@ -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) {
|
|
@@ -155,14 +164,14 @@ $cutoffFilter = $cutoff.ToString('g').Replace("'", "''")
|
|
|
155
164
|
`;
|
|
156
165
|
}
|
|
157
166
|
/**
|
|
158
|
-
*
|
|
167
|
+
* `$filtered` = the items of `$<folderVariable>` with `property` on or after
|
|
159
168
|
* `$cutoff`. Requires `cutoffScript`. A store that rejects the filter outright
|
|
160
169
|
* yields every item instead, which the per-item re-check then narrows.
|
|
161
170
|
*/
|
|
162
|
-
function itemsSinceScript(folderVariable, property
|
|
171
|
+
function itemsSinceScript(folderVariable, property) {
|
|
163
172
|
return `
|
|
164
|
-
|
|
165
|
-
try {
|
|
173
|
+
$filtered = $null
|
|
174
|
+
try { $filtered = $${folderVariable}.Items.Restrict("[${property}] >= '$cutoffFilter'") } catch { $filtered = $${folderVariable}.Items }
|
|
166
175
|
`;
|
|
167
176
|
}
|
|
168
177
|
/**
|
|
@@ -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,EAAE,QAAQ,GAAG,MAAM;IACnE,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO;QACzB,CAAC,CAAC,UAAU,QAAQ,mCAAmC,IAAA,cAAQ,EAAC,KAAK,CAAC,OAAO,CAAC,gBAAgB,QAAQ,YAAY;QAClH,CAAC,CAAC,EAAE,CAAC;IACT,OAAO;cACG,IAAA,cAAQ,EAAC,KAAK,CAAC,OAAO,CAAC;GAClC,QAAQ;EACT,OAAO;OACF,QAAQ,wBAAwB,QAAQ,8CAA8C,QAAQ;OAC9F,QAAQ;aACF,IAAA,mBAAU,EAAC,WAAW,EAAE,OAAO,CAAC;;CAE5C,CAAC;AACF,CAAC;AAED;;;;;GAKG;AACU,QAAA,cAAc,GAAG;;;;;;CAM7B,CAAC;AAEF,oGAAoG;AACvF,QAAA,cAAc,GAAG;;;;;;;;;;;;CAY7B,CAAC;AAEF;;;;;;;;;GASG;AACH,SAAgB,YAAY,CAAC,QAAgB;IACzC,OAAO;gCACqB,IAAA,WAAK,EAAC,QAAQ,CAAC;;CAE9C,CAAC;AACF,CAAC;AAED;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,cAAsB,EAAE,QAAgB,EAAE,QAAgB;IACvF,OAAO;GACR,QAAQ;SACF,QAAQ,OAAO,cAAc,qBAAqB,QAAQ,qCAAqC,QAAQ,OAAO,cAAc;CACpI,CAAC;AACF,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,eAAe,CAAC,GAAkB,EAAE,WAAmB,EAAE,aAAa,GAAG,KAAK;IAC1F,MAAM,IAAI,GAAG,2BAA2B,IAAA,WAAK,EAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC;IAC7D,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,YAAY,IAAI,0BAA0B,CAAC;IACjF,OAAO;EACT,sBAAc;iBACC,IAAA,cAAQ,EAAC,WAAW,CAAC;gBACtB,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,cAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;eACtC,IAAI;;;;;;;;;;;;;4BAaS,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ;;;;;;;;;;;;;aAajD,IAAA,mBAAU,EAAC,WAAW,EAAE,QAAQ,CAAC;EAC5C,CAAC;AACH,CAAC","sourcesContent":["// The PowerShell fragments more than one Windows operation is built from.\n//\n// A fragment lives here once two modules emit it; one used by a single\n// operation stays beside that operation, where its rules read next to the code\n// relying on them. Every fragment follows run.ts's escaping rule: caller text\n// only ever appears as a psString literal.\nimport { psInt, psString } from './run';\nimport { failureTag } from '../errors';\nimport type { EmailLocator, MailFolderRef } from '../types';\n\n/**\n * Open the MAPI session, binding `$outlook` and `$ns`.\n *\n * `Logon()` is a no-op against an Outlook that is already running and starts\n * one that isn't, which removes a class of \"works only while Outlook is open\"\n * failure.\n */\nexport const SESSION_PS = `\n$outlook = New-Object -ComObject Outlook.Application\n$ns = $outlook.GetNamespace('MAPI')\n$ns.Logon()\n`;\n\n/**\n * The session plus the mailbox an address names; binds `$target`, `$account`\n * and `$storeFolder`.\n *\n * An address can name a mailbox two ways, and only one is an Account. A mailbox\n * mounted as a secondary store — a shared mailbox, or one added after Outlook\n * started — has no Account object, so both are resolved and EITHER is enough:\n * `$account` is null for a store-only mailbox, and the operations that need an\n * identity to act as (sending) check for it themselves.\n *\n * Never falls back to the default account: a bad address must not silently\n * read or send from the wrong mailbox.\n *\n * The store is matched on the address BEFORE the account's display name, so a\n * profile where the two differ selects the same folders it always has.\n */\nexport function accountScript(emailAccount: string): string {\n return `${SESSION_PS}\n$target = ${psString(emailAccount)}\n$account = $null\nforeach ($a in $ns.Accounts) {\n if ($a.SmtpAddress -ieq $target) { $account = $a; break }\n}\n$storeFolder = $null\nforeach ($f in $ns.Folders) {\n if ($f.Name -ieq $target) { $storeFolder = $f; break }\n}\nif ($storeFolder -eq $null -and $account -ne $null) {\n foreach ($f in $ns.Folders) {\n if ($f.Name -ieq $account.DisplayName) { $storeFolder = $f; break }\n }\n}\nif ($account -eq $null -and $storeFolder -eq $null) {\n throw \"${failureTag('ACCOUNT_NOT_FOUND')}Account '$target' not found in this Outlook profile.\"\n}\n`;\n}\n\n/**\n * The mailbox's store as `$store` (and its id as `$storeId`), reached through\n * the account's delivery store. Requires `accountScript`.\n *\n * Falls back to the resolved store folder, because a store-only mailbox has no\n * account to read a delivery store off.\n */\nexport const DELIVERY_STORE_PS = `\n$store = if ($account -ne $null) { $account.DeliveryStore } else { $storeFolder.Store }\n$storeId = $store.StoreID\n`;\n\n/**\n * The mailbox's store as `$store`, reached through the namespace's top-level\n * folder for it. Requires `accountScript`.\n *\n * Kept beside DELIVERY_STORE_PS rather than replaced by it: the readers resolve\n * their store this way, and on a profile where a mailbox is open under a display\n * name that differs from its delivery store's, the two routes do not select the\n * same folders. Each operation keeps the route it was verified against.\n */\nexport const NAMED_STORE_PS = `\nif ($storeFolder -eq $null) {\n throw \"${failureTag('ACCOUNT_NOT_FOUND')}Account '$target' has no mailbox open in this Outlook profile.\"\n}\n$store = $storeFolder.Store\n$storeId = $store.StoreID\n`;\n\n/**\n * Resolve one email into `$<variable>`, or fail as NOT_FOUND.\n *\n * The StoreID is tried first, since it lets an id from any mounted mailbox\n * resolve; the bare lookup (default store) is the fallback for a caller holding\n * a stale StoreID or none.\n */\nexport function itemLookupScript(email: EmailLocator, variable = 'item'): string {\n const byStore = email.storeId\n ? `try { $${variable} = $ns.GetItemFromID($lookupId, ${psString(email.storeId)}) } catch { $${variable} = $null }`\n : '';\n return `\n$lookupId = ${psString(email.entryId)}\n$${variable} = $null\n${byStore}\nif ($${variable} -eq $null) { try { $${variable} = $ns.GetItemFromID($lookupId) } catch { $${variable} = $null } }\nif ($${variable} -eq $null) {\n throw \"${failureTag('NOT_FOUND', 'email')}No email found for entry id '$lookupId'. It may have been moved or deleted - list the mail again for a current id.\"\n}\n`;\n}\n\n/**\n * The sender's SMTP address as `$senderSmtp`. Requires `$item`.\n *\n * An Exchange sender's SenderEmailAddress is an X.500 DN, not an address; a\n * caller replying to it or matching it against anything needs the address.\n */\nexport const SENDER_SMTP_PS = `\n$senderSmtp = ''\ntry {\n if ($item.SenderEmailType -eq 'EX') { $senderSmtp = [string]$item.Sender.GetExchangeUser().PrimarySmtpAddress }\n} catch {}\nif (-not $senderSmtp) { try { $senderSmtp = [string]$item.SenderEmailAddress } catch {} }\n`;\n\n/** Recursive, case-insensitive folder search by name, depth-capped so a huge tree can't hang it. */\nexport const FIND_FOLDER_PS = `\nfunction Find-FolderByName($root, [string]$name, [int]$depth) {\n foreach ($f in $root.Folders) {\n if ($f.Name -ieq $name) { return $f }\n }\n if ($depth -le 1) { return $null }\n foreach ($f in $root.Folders) {\n $hit = Find-FolderByName $f $name ($depth - 1)\n if ($hit -ne $null) { return $hit }\n }\n return $null\n}\n`;\n\n/**\n * `$cutoff`, the moment `daysBack` days ago, and `$cutoffFilter`, that moment\n * as a date string for an `Items.Restrict` filter.\n *\n * Restrict parses date strings in the machine's own regional format, so the\n * string is formatted in it ('g': short date and time) rather than a fixed\n * pattern — 'MM/dd/yyyy' reads as a different day on a day-first locale. Every\n * reader re-checks each item against `$cutoff` too, so the window holds even\n * where a store reads the filter loosely.\n */\nexport function cutoffScript(daysBack: number): string {\n return `\n$cutoff = (Get-Date).AddDays(-${psInt(daysBack)})\n$cutoffFilter = $cutoff.ToString('g').Replace(\"'\", \"''\")\n`;\n}\n\n/**\n * `$<variable>` = the items of `$<folderVariable>` with `property` on or after\n * `$cutoff`. Requires `cutoffScript`. A store that rejects the filter outright\n * yields every item instead, which the per-item re-check then narrows.\n */\nexport function itemsSinceScript(folderVariable: string, property: string, variable: string): string {\n return `\n$${variable} = $null\ntry { $${variable} = $${folderVariable}.Items.Restrict(\"[${property}] >= '$cutoffFilter'\") } catch { $${variable} = $${folderVariable}.Items }\n`;\n}\n\n/**\n * Resolve `$scope` from `$store`: a well-known root walked down through any\n * further segments. Also binds `$scopeCreated`.\n *\n * Segments match direct children; a bare name that isn't a direct child falls\n * back to a recursive search, so one folder string works across operations. A\n * missing folder fails as NOT_FOUND naming what the caller typed and what the\n * deepest folder reached does hold — never a silent fall back to the root. With\n * `createMissing`, the whole missing chain is created instead.\n */\nexport function mailScopeScript(ref: MailFolderRef, folderLabel: string, createMissing = false): string {\n const root = `$store.GetDefaultFolder(${psInt(ref.rootId)})`;\n if (ref.segments.length === 0) return `$scope = ${root}\\n$scopeCreated = $false`;\n return `\n${FIND_FOLDER_PS}\n$folderLabel = ${psString(folderLabel)}\n$segments = @(${ref.segments.map(psString).join(', ')})\n$scopeRoot = ${root}\n$scope = $scopeRoot\n$deepest = $scopeRoot\n$scopeCreated = $false\nforeach ($seg in $segments) {\n $next = $null\n foreach ($f in $scope.Folders) { if ($f.Name -ieq $seg) { $next = $f; break } }\n if ($next -eq $null) { $deepest = $scope; $scope = $null; break }\n $scope = $next\n}\nif ($scope -eq $null -and $segments.Count -eq 1) {\n $scope = Find-FolderByName $scopeRoot $segments[0] 3\n}\nif ($scope -eq $null -and ${createMissing ? '$true' : '$false'}) {\n # Rebuild the chain from the root, creating only what is genuinely absent.\n $scope = $scopeRoot\n foreach ($seg in $segments) {\n $next = $null\n foreach ($f in $scope.Folders) { if ($f.Name -ieq $seg) { $next = $f; break } }\n if ($next -eq $null) { $next = $scope.Folders.Add($seg); $scopeCreated = $true }\n $scope = $next\n }\n}\nif ($scope -eq $null) {\n $names = @()\n foreach ($f in $deepest.Folders) { $names += $f.Name }\n throw \"${failureTag('NOT_FOUND', 'folder')}Folder '$folderLabel' not found. Folders under '$($deepest.Name)': $($names -join ', ')\"\n}`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"scripts.js","sourceRoot":"","sources":["../../src/windows/scripts.ts"],"names":[],"mappings":";;;AAuCA,sCAoBC;AAsCD,4CAaC;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
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { ReplyRequest, SendRequest } from '../backend';
|
|
2
|
-
import type { ReplyEmailResult } from '../types';
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import type { ReplyEmailResult, SendEmailResult } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Send, display or file a new email. Only a filed draft reports an id: a sent
|
|
5
|
+
* item has left for the Outbox, and a displayed one is saved nowhere yet. The
|
|
6
|
+
* id is read after `Save()`, which is what assigns it — the EntryID the
|
|
7
|
+
* draft then keeps, and the one listOutlookDrafts reports.
|
|
8
|
+
*/
|
|
9
|
+
export declare function sendOutlookEmail(request: SendRequest): Promise<SendEmailResult>;
|
|
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
|
+
*/
|
|
6
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
|
*
|
|
@@ -45,7 +45,12 @@ function dispose(variable, disposition) {
|
|
|
45
45
|
function recipientLine(entries) {
|
|
46
46
|
return (0, run_1.psString)(entries.join('; '));
|
|
47
47
|
}
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Send, display or file a new email. Only a filed draft reports an id: a sent
|
|
50
|
+
* item has left for the Outbox, and a displayed one is saved nowhere yet. The
|
|
51
|
+
* id is read after `Save()`, which is what assigns it — the EntryID the
|
|
52
|
+
* draft then keeps, and the one listOutlookDrafts reports.
|
|
53
|
+
*/
|
|
49
54
|
async function sendOutlookEmail(request) {
|
|
50
55
|
// The body goes through a file: inline, a large email bloats the script and
|
|
51
56
|
// every error that carries it.
|
|
@@ -53,8 +58,9 @@ async function sendOutlookEmail(request) {
|
|
|
53
58
|
const attach = request.attachments
|
|
54
59
|
.map(file => `[void]$mail.Attachments.Add(${(0, run_1.psString)(file)})`)
|
|
55
60
|
.join('\n');
|
|
61
|
+
const draftEntryId = request.disposition === 'save' ? '[string]$mail.EntryID' : "''";
|
|
56
62
|
try {
|
|
57
|
-
await (0, run_1.
|
|
63
|
+
const output = await (0, run_1.runPowerShellJson)(`${(0, scripts_1.accountScript)(request.account)}
|
|
58
64
|
$mail = $outlook.CreateItem(0)
|
|
59
65
|
${sendUsingAccount('mail')}
|
|
60
66
|
$mail.To = ${recipientLine(request.to)}
|
|
@@ -64,15 +70,22 @@ $mail.Subject = ${(0, run_1.psString)(request.subject)}
|
|
|
64
70
|
$mail.HTMLBody = [IO.File]::ReadAllText(${(0, run_1.psString)(body.path)}, [Text.Encoding]::UTF8)
|
|
65
71
|
${attach}
|
|
66
72
|
${dispose('mail', request.disposition)}
|
|
73
|
+
ConvertTo-Json -Compress @{ draftEntryId = ${draftEntryId} }
|
|
67
74
|
`, 'standard');
|
|
75
|
+
return { draftEntryId: (0, json_1.str)((0, json_1.record)(output).draftEntryId) };
|
|
68
76
|
}
|
|
69
77
|
finally {
|
|
70
78
|
body.cleanup();
|
|
71
79
|
}
|
|
72
80
|
}
|
|
73
|
-
/**
|
|
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
|
+
*/
|
|
74
86
|
async function replyOutlookEmail(request) {
|
|
75
87
|
const insert = (0, run_1.scriptInput)('reply-insert', request.html);
|
|
88
|
+
const draftEntryId = request.disposition === 'save' ? '[string]$reply.EntryID' : "''";
|
|
76
89
|
try {
|
|
77
90
|
const output = await (0, run_1.runPowerShellJson)(`${(0, scripts_1.accountScript)(request.account)}
|
|
78
91
|
${(0, scripts_1.itemLookupScript)(request.email)}
|
|
@@ -93,10 +106,15 @@ $to = [string]$reply.To
|
|
|
93
106
|
$subject = [string]$reply.Subject
|
|
94
107
|
${scripts_1.SENDER_SMTP_PS}
|
|
95
108
|
${dispose('reply', request.disposition)}
|
|
96
|
-
ConvertTo-Json -Compress @{ to = $to; subject = $subject; repliedToSender = $senderSmtp }
|
|
109
|
+
ConvertTo-Json -Compress @{ to = $to; subject = $subject; repliedToSender = $senderSmtp; draftEntryId = ${draftEntryId} }
|
|
97
110
|
`, 'standard');
|
|
98
111
|
const result = (0, json_1.record)(output);
|
|
99
|
-
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
|
+
};
|
|
100
118
|
}
|
|
101
119
|
finally {
|
|
102
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":";;
|
|
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,17 +7,12 @@ 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
|
+
const templateLimits_1 = require("../shared/templateLimits");
|
|
11
12
|
const templateBody_1 = require("../templateBody");
|
|
12
13
|
const mail_1 = require("../mail");
|
|
13
14
|
const errors_1 = require("../errors");
|
|
14
15
|
const runtime_1 = require("../runtime");
|
|
15
|
-
/** How deep under the mailbox root a template folder is searched for. */
|
|
16
|
-
const TEMPLATE_SEARCH_DEPTH = 3;
|
|
17
|
-
/** Cap on one template body; a Word-generated one is large, and a runaway one shouldn't sink the call. */
|
|
18
|
-
const MAX_TEMPLATE_HTML = 100_000;
|
|
19
|
-
/** Plain text standing in for a body that wasn't asked for. */
|
|
20
|
-
const PREVIEW_CHARS = 200;
|
|
21
16
|
/**
|
|
22
17
|
* The template emails in a mailbox folder, newest first. A missing folder is
|
|
23
18
|
* reported as folderFound:false with the mailbox's top-level folder names
|
|
@@ -28,7 +23,7 @@ async function readTemplateEmails(request) {
|
|
|
28
23
|
${scripts_1.DELIVERY_STORE_PS}
|
|
29
24
|
$root = $store.GetRootFolder()
|
|
30
25
|
${scripts_1.FIND_FOLDER_PS}
|
|
31
|
-
$folder = Find-FolderByName $root ${(0, run_1.psString)(request.folder)} ${(0, run_1.psInt)(TEMPLATE_SEARCH_DEPTH)}
|
|
26
|
+
$folder = Find-FolderByName $root ${(0, run_1.psString)(request.folder)} ${(0, run_1.psInt)(templateLimits_1.TEMPLATE_SEARCH_DEPTH)}
|
|
32
27
|
if ($folder -eq $null) {
|
|
33
28
|
$names = @()
|
|
34
29
|
foreach ($f in $root.Folders) { $names += [string]$f.Name }
|
|
@@ -38,8 +33,8 @@ if ($folder -eq $null) {
|
|
|
38
33
|
$includeBody = ${(0, run_1.psBool)(request.includeBody)}
|
|
39
34
|
$wanted = ${(0, run_1.psString)(request.subject ?? '')}
|
|
40
35
|
$limit = ${(0, run_1.psInt)(request.limit)}
|
|
41
|
-
$maxHtml = ${(0, run_1.psInt)(MAX_TEMPLATE_HTML)}
|
|
42
|
-
$previewChars = ${(0, run_1.psInt)(
|
|
36
|
+
$maxHtml = ${(0, run_1.psInt)(templateLimits_1.MAX_TEMPLATE_HTML)}
|
|
37
|
+
$previewChars = ${(0, run_1.psInt)(templateLimits_1.TEMPLATE_PREVIEW_CHARS)}
|
|
43
38
|
$items = $folder.Items
|
|
44
39
|
$items.Sort('[LastModificationTime]', $true)
|
|
45
40
|
$rows = @()
|
|
@@ -61,10 +56,9 @@ for ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {
|
|
|
61
56
|
$markers = ''
|
|
62
57
|
if ($includeBody) {
|
|
63
58
|
try { $html = [string]$item.HTMLBody } catch {}
|
|
64
|
-
# Embedded (cid:) images
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
$html = [regex]::Replace($html, '<img[^>]*src="cid:[^"]*"[^>]*>', '')
|
|
59
|
+
# Embedded (cid:) images are attachments of the TEMPLATE item, so they
|
|
60
|
+
# would show as broken placeholders anywhere else.
|
|
61
|
+
$html = [regex]::Replace($html, ${(0, run_1.psString)(templateLimits_1.EMBEDDED_IMAGE_SOURCE)}, '', [Text.RegularExpressions.RegexOptions]::IgnoreCase)
|
|
68
62
|
if ($html.Length -gt $maxHtml) { $html = $html.Substring(0, $maxHtml) }
|
|
69
63
|
} else {
|
|
70
64
|
# No body asked for: a short preview, plus the section and placeholder
|
|
@@ -74,7 +68,7 @@ for ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {
|
|
|
74
68
|
$plain = ''
|
|
75
69
|
try { $plain = [string]$item.Body } catch {}
|
|
76
70
|
$found = @()
|
|
77
|
-
foreach ($m in [regex]::Matches($plain,
|
|
71
|
+
foreach ($m in [regex]::Matches($plain, ${(0, run_1.psString)(templateBody_1.MARKER_TOKEN_SOURCE)})) {
|
|
78
72
|
$token = $m.Value -replace '\\s', ''
|
|
79
73
|
if ($found -notcontains $token) { $found += $token }
|
|
80
74
|
}
|
|
@@ -83,7 +77,7 @@ for ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {
|
|
|
83
77
|
if ($preview.Length -gt $previewChars) { $preview = $preview.Substring(0, $previewChars) }
|
|
84
78
|
}
|
|
85
79
|
$modified = ''
|
|
86
|
-
try { $modified = $item.LastModificationTime
|
|
80
|
+
try { $modified = ${(0, scripts_1.stampPs)('$item.LastModificationTime')} } catch {}
|
|
87
81
|
$rows += [PSCustomObject]@{
|
|
88
82
|
entryId = [string]$item.EntryID
|
|
89
83
|
subject = $subject
|
|
@@ -136,7 +130,7 @@ ${scripts_1.DELIVERY_STORE_PS}
|
|
|
136
130
|
$root = $store.GetRootFolder()
|
|
137
131
|
${scripts_1.FIND_FOLDER_PS}
|
|
138
132
|
$folderName = ${(0, run_1.psString)(request.folder)}
|
|
139
|
-
$folder = Find-FolderByName $root $folderName ${(0, run_1.psInt)(TEMPLATE_SEARCH_DEPTH)}
|
|
133
|
+
$folder = Find-FolderByName $root $folderName ${(0, run_1.psInt)(templateLimits_1.TEMPLATE_SEARCH_DEPTH)}
|
|
140
134
|
$folderCreated = $false
|
|
141
135
|
if ($folder -eq $null) {
|
|
142
136
|
$folder = $root.Folders.Add($folderName)
|