@dawnlit/outlook-bridge 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +6 -4
  2. package/dist/backend.d.ts +2 -2
  3. package/dist/backend.js.map +1 -1
  4. package/dist/bridge.js +3 -3
  5. package/dist/bridge.js.map +1 -1
  6. package/dist/errors.d.ts +0 -2
  7. package/dist/errors.js +1 -6
  8. package/dist/errors.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/mac/accounts.js.map +1 -1
  12. package/dist/mac/attachments.js +1 -1
  13. package/dist/mac/attachments.js.map +1 -1
  14. package/dist/mac/bounces.js +8 -12
  15. package/dist/mac/bounces.js.map +1 -1
  16. package/dist/mac/cleanup.js +4 -41
  17. package/dist/mac/cleanup.js.map +1 -1
  18. package/dist/mac/drafts.d.ts +3 -1
  19. package/dist/mac/drafts.js +34 -33
  20. package/dist/mac/drafts.js.map +1 -1
  21. package/dist/mac/folders.d.ts +5 -3
  22. package/dist/mac/folders.js +18 -19
  23. package/dist/mac/folders.js.map +1 -1
  24. package/dist/mac/profile.js.map +1 -1
  25. package/dist/mac/read.d.ts +4 -0
  26. package/dist/mac/read.js +54 -16
  27. package/dist/mac/read.js.map +1 -1
  28. package/dist/mac/run.d.ts +19 -2
  29. package/dist/mac/run.js +42 -2
  30. package/dist/mac/run.js.map +1 -1
  31. package/dist/mac/scripts.d.ts +21 -14
  32. package/dist/mac/scripts.js +52 -21
  33. package/dist/mac/scripts.js.map +1 -1
  34. package/dist/mac/send.d.ts +3 -3
  35. package/dist/mac/send.js +44 -12
  36. package/dist/mac/send.js.map +1 -1
  37. package/dist/mac/templates.js +15 -18
  38. package/dist/mac/templates.js.map +1 -1
  39. package/dist/mail.d.ts +5 -3
  40. package/dist/mail.js +19 -6
  41. package/dist/mail.js.map +1 -1
  42. package/dist/runtime.js.map +1 -1
  43. package/dist/shared/args.js.map +1 -1
  44. package/dist/shared/attachmentMatch.d.ts +0 -6
  45. package/dist/shared/attachmentMatch.js +0 -1
  46. package/dist/shared/attachmentMatch.js.map +1 -1
  47. package/dist/shared/exec.js.map +1 -1
  48. package/dist/shared/json.d.ts +11 -6
  49. package/dist/shared/json.js +12 -7
  50. package/dist/shared/json.js.map +1 -1
  51. package/dist/shared/locate.d.ts +0 -8
  52. package/dist/shared/locate.js +0 -1
  53. package/dist/shared/locate.js.map +1 -1
  54. package/dist/shared/replyBody.d.ts +0 -15
  55. package/dist/shared/replyBody.js +0 -2
  56. package/dist/shared/replyBody.js.map +1 -1
  57. package/dist/shared/templateLimits.d.ts +14 -0
  58. package/dist/shared/templateLimits.js +21 -0
  59. package/dist/shared/templateLimits.js.map +1 -0
  60. package/dist/templateBody.d.ts +6 -0
  61. package/dist/templateBody.js +8 -1
  62. package/dist/templateBody.js.map +1 -1
  63. package/dist/types.d.ts +23 -3
  64. package/dist/types.js.map +1 -1
  65. package/dist/windows/bounces.js +4 -4
  66. package/dist/windows/bounces.js.map +1 -1
  67. package/dist/windows/cleanup.d.ts +5 -1
  68. package/dist/windows/cleanup.js +16 -11
  69. package/dist/windows/cleanup.js.map +1 -1
  70. package/dist/windows/drafts.d.ts +4 -4
  71. package/dist/windows/drafts.js +20 -31
  72. package/dist/windows/drafts.js.map +1 -1
  73. package/dist/windows/folders.js +1 -1
  74. package/dist/windows/folders.js.map +1 -1
  75. package/dist/windows/read.js +6 -2
  76. package/dist/windows/read.js.map +1 -1
  77. package/dist/windows/run.d.ts +0 -2
  78. package/dist/windows/run.js +1 -2
  79. package/dist/windows/run.js.map +1 -1
  80. package/dist/windows/scripts.d.ts +4 -4
  81. package/dist/windows/scripts.js +10 -10
  82. package/dist/windows/scripts.js.map +1 -1
  83. package/dist/windows/send.d.ts +8 -3
  84. package/dist/windows/send.js +10 -2
  85. package/dist/windows/send.js.map +1 -1
  86. package/dist/windows/templates.js +9 -15
  87. package/dist/windows/templates.js.map +1 -1
  88. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"bounces.js","sourceRoot":"","sources":["../../src/windows/bounces.ts"],"names":[],"mappings":"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","sourcesContent":["// Bounce-backs: finding them, clearing them away, and mining them for the\n// addresses that failed.\nimport { psArray, psBool, psInt, psString, runPowerShellJson } from './run';\nimport { accountScript, cutoffScript, DELIVERY_STORE_PS, itemsSinceScript, NAMED_STORE_PS } from './scripts';\nimport { itemFailures, num, record, str, strList, toArray } from '../shared/json';\nimport {\n BODY_ADDRESS_PATTERN,\n BOUNCE_DAEMON_ADDRESSES,\n BOUNCE_DAEMON_NAMES,\n BOUNCE_REASON,\n BOUNCE_SUBJECT_PHRASES,\n DAEMON_SELF_ADDRESSES,\n MAX_FAILED_RECIPIENTS,\n NDR_MESSAGE_CLASS_PREFIX,\n} from '../shared/bounceRules';\nimport { FolderId } from '../mail';\nimport type { CleanUndeliverableRequest, CollectBouncesRequest, SentGroupsRequest } from '../backend';\nimport type { CleanUndeliverableResult, SentRecipientGroup } from '../types';\n\n/**\n * The classifier, generated from the shared rules so Windows and macOS can't\n * disagree about what a bounce is. Defines `Get-BounceReport`, which returns\n * null for an item that isn't a bounce and a report row for one that is.\n *\n * The body is read only after a cheaper signal (class, sender, subject) has\n * matched — that ordering is what keeps a large-inbox scan affordable.\n */\nfunction bounceClassifierScript(): string {\n return `\n$bouncePhrases = ${psArray(BOUNCE_SUBJECT_PHRASES)}\n$daemonAddresses = ${psArray(BOUNCE_DAEMON_ADDRESSES)}\n$daemonNames = ${psArray(BOUNCE_DAEMON_NAMES)}\n$daemonSelf = ${psArray(DAEMON_SELF_ADDRESSES)}\n$ndrPrefix = ${psString(NDR_MESSAGE_CLASS_PREFIX)}\n$addressPattern = ${psString(BODY_ADDRESS_PATTERN)}\n$maxFailed = ${psInt(MAX_FAILED_RECIPIENTS)}\n$accountLower = $target.ToLower()\nfunction Get-BounceReport($item) {\n $class = 0\n try { $class = [int]$item.Class } catch {}\n if ($class -ne 43 -and $class -ne 46) { return $null }\n $subject = ''\n try { $subject = [string]$item.Subject } catch {}\n $senderName = ''\n try { $senderName = [string]$item.SenderName } catch {}\n $senderEmail = ''\n try { $senderEmail = [string]$item.SenderEmailAddress } catch {}\n $messageClass = ''\n try { $messageClass = [string]$item.MessageClass } catch {}\n $reason = ''\n if ($messageClass.StartsWith($ndrPrefix, [StringComparison]::OrdinalIgnoreCase)) { $reason = ${psString(BOUNCE_REASON.ndr)} }\n if (-not $reason) { foreach ($d in $daemonAddresses) { if ($senderEmail.ToLower().Contains($d)) { $reason = ${psString(BOUNCE_REASON.daemon)}; break } } }\n if (-not $reason) { foreach ($d in $daemonNames) { if ($senderName.ToLower().Contains($d)) { $reason = ${psString(BOUNCE_REASON.daemon)}; break } } }\n if (-not $reason) { foreach ($p in $bouncePhrases) { if ($subject.ToLower().Contains($p)) { $reason = \"${BOUNCE_REASON.subjectPhrase('$p')}\"; break } } }\n if (-not $reason) { return $null }\n $failedRecipients = @()\n $body = ''\n try { $body = [string]$item.Body } catch {}\n foreach ($m in [regex]::Matches($body, $addressPattern)) {\n $address = $m.Value.ToLower().TrimEnd('.')\n if ($address -eq $accountLower) { continue }\n $isDaemon = $false\n foreach ($d in $daemonSelf) { if ($address.Contains($d)) { $isDaemon = $true; break } }\n if ($isDaemon -or $failedRecipients -contains $address) { continue }\n $failedRecipients += $address\n if ($failedRecipients.Count -ge $maxFailed) { break }\n }\n $received = ''\n try { $received = $item.ReceivedTime.ToString('yyyy-MM-dd HH:mm') } catch {}\n return [PSCustomObject]@{\n entryId = [string]$item.EntryID\n subject = $subject.Trim()\n senderName = $senderName\n senderEmail = $senderEmail\n receivedTime = $received\n matchedReason = $reason\n failedRecipients = @($failedRecipients)\n }\n}\n`;\n}\n\n/**\n * Scan an account's Inbox for bounce-backs and, unless dry-running, move each to\n * Deleted Items (recoverable).\n *\n * Matched items are collected before the first Delete(): deleting shifts the\n * collection, and deleting mid-enumeration would skip items.\n */\nexport async function cleanUndeliverableEmails(request: CleanUndeliverableRequest): Promise<CleanUndeliverableResult> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${NAMED_STORE_PS}\n${bounceClassifierScript()}\n$dryRun = ${psBool(request.dryRun)}\n$inbox = $store.GetDefaultFolder(${psInt(FolderId.Inbox)})\n${cutoffScript(request.daysBack)}\n${itemsSinceScript('inbox', 'ReceivedTime', 'filtered')}\n$reports = @()\n$matchedItems = @()\n$count = $filtered.Count\nfor ($i = 1; $i -le $count; $i++) {\n $item = $null\n try { $item = $filtered.Item($i) } catch { continue }\n if ($item -eq $null) { continue }\n $received = $null\n try { $received = $item.ReceivedTime } catch {}\n if ($received -ne $null -and $received -lt $cutoff) { continue }\n $report = Get-BounceReport $item\n if ($report -eq $null) { continue }\n $reports += $report\n $matchedItems += $item\n}\n$deleted = 0\n$failed = @()\nif (-not $dryRun) {\n foreach ($m in $matchedItems) {\n $entryId = ''\n $subject = ''\n try { $entryId = [string]$m.EntryID } catch {}\n try { $subject = [string]$m.Subject } catch {}\n try { $m.Delete(); $deleted++ }\n catch { $failed += [PSCustomObject]@{ entryId = $entryId; subject = $subject; error = $_.Exception.Message } }\n }\n}\nConvertTo-Json -Depth 4 -InputObject ([PSCustomObject]@{\n deletedCount = $deleted\n matched = @($reports)\n failed = @($failed)\n})\n`, 'scan');\n const e = record(output);\n const matched = toArray(e.matched).map(row => {\n const m = record(row);\n return {\n entryId: str(m.entryId),\n subject: str(m.subject),\n senderName: str(m.senderName),\n senderEmail: str(m.senderEmail),\n receivedTime: str(m.receivedTime),\n matchedReason: str(m.matchedReason),\n failedRecipients: strList(m.failedRecipients),\n };\n });\n return {\n account: request.account,\n scannedDays: request.daysBack,\n dryRun: request.dryRun,\n matchedCount: matched.length,\n deletedCount: num(e.deletedCount),\n matched,\n failed: itemFailures(e.failed),\n };\n}\n\n/**\n * The deduplicated addresses bounce-backs report as failed, from the Inbox and,\n * optionally, Deleted Items — so it still works after the bounces were cleaned\n * away. Never deletes anything.\n */\nexport async function collectBouncedRecipients(request: CollectBouncesRequest): Promise<string[]> {\n const roles = request.includeDeletedItems ? [FolderId.Inbox, FolderId.DeletedItems] : [FolderId.Inbox];\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n${bounceClassifierScript()}\n${cutoffScript(request.daysBack)}\n$found = [ordered]@{}\nforeach ($role in @(${roles.map(psInt).join(', ')})) {\n $scanFolder = $null\n try { $scanFolder = $store.GetDefaultFolder($role) } catch {}\n if ($scanFolder -eq $null) { continue }\n ${itemsSinceScript('scanFolder', 'ReceivedTime', 'filtered').trim()}\n $count = $filtered.Count\n for ($i = 1; $i -le $count; $i++) {\n $item = $null\n try { $item = $filtered.Item($i) } catch { continue }\n if ($item -eq $null) { continue }\n $received = $null\n try { $received = $item.ReceivedTime } catch {}\n if ($received -ne $null -and $received -lt $cutoff) { continue }\n $report = Get-BounceReport $item\n if ($report -eq $null) { continue }\n foreach ($address in $report.failedRecipients) { $found[$address] = $true }\n }\n}\nConvertTo-Json -Compress -InputObject @($found.Keys)\n`, 'scan');\n return strList(output);\n}\n\n/**\n * Sent Items within the window, each message with the full SMTP address set it\n * went to (To + CC + BCC), newest first.\n */\nexport async function readSentRecipientGroups(request: SentGroupsRequest): Promise<SentRecipientGroup[]> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n$sentFolder = $store.GetDefaultFolder(${psInt(FolderId.SentMail)})\n${cutoffScript(request.daysBack)}\n${itemsSinceScript('sentFolder', 'SentOn', 'filtered')}\n$filtered.Sort('[SentOn]', $true)\n$limit = ${psInt(request.limit)}\n$rows = @()\n$count = $filtered.Count\nfor ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {\n $m = $null\n try { $m = $filtered.Item($i) } catch { continue }\n if ($m -eq $null) { continue }\n $sentOn = $null\n try { $sentOn = $m.SentOn } catch {}\n if ($sentOn -ne $null -and $sentOn -lt $cutoff) { break }\n $class = 0\n try { $class = [int]$m.Class } catch {}\n if ($class -ne 43) { continue }\n $addresses = @()\n try {\n foreach ($r in $m.Recipients) {\n $address = ''\n try { $address = [string]$r.Address } catch {}\n # An internal recipient resolves to an Exchange DN; recover its SMTP address.\n if ($address -notlike '*@*') {\n try { $address = [string]$r.AddressEntry.GetExchangeUser().PrimarySmtpAddress } catch {}\n }\n if ($address -like '*@*') { $addresses += $address.ToLower() }\n }\n } catch {}\n if ($addresses.Count -eq 0) { continue }\n $rows += [PSCustomObject]@{\n entryId = [string]$m.EntryID\n subject = ([string]$m.Subject).Trim()\n sentOn = if ($sentOn -ne $null) { $sentOn.ToString('yyyy-MM-dd HH:mm') } else { '' }\n recipients = @($addresses)\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 subject: str(e.subject),\n sentOn: str(e.sentOn),\n recipients: strList(e.recipients),\n };\n });\n}\n"]}
1
+ {"version":3,"file":"bounces.js","sourceRoot":"","sources":["../../src/windows/bounces.ts"],"names":[],"mappings":"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","sourcesContent":["// Bounce-backs: finding them, clearing them away, and mining them for the\n// addresses that failed.\nimport { psArray, psBool, psInt, psString, runPowerShellJson } from './run';\nimport { accountScript, cutoffScript, DELIVERY_STORE_PS, itemsSinceScript, NAMED_STORE_PS } from './scripts';\nimport { itemFailurePs, itemFailures, num, record, str, strList, toArray } from '../shared/json';\nimport {\n BODY_ADDRESS_PATTERN,\n BOUNCE_DAEMON_ADDRESSES,\n BOUNCE_DAEMON_NAMES,\n BOUNCE_REASON,\n BOUNCE_SUBJECT_PHRASES,\n DAEMON_SELF_ADDRESSES,\n MAX_FAILED_RECIPIENTS,\n NDR_MESSAGE_CLASS_PREFIX,\n} from '../shared/bounceRules';\nimport { FolderId } from '../mail';\nimport type { CleanUndeliverableRequest, CollectBouncesRequest, SentGroupsRequest } from '../backend';\nimport type { CleanUndeliverableResult, SentRecipientGroup } from '../types';\n\n/**\n * The classifier, generated from the shared rules so Windows and macOS can't\n * disagree about what a bounce is. Defines `Get-BounceReport`, which returns\n * null for an item that isn't a bounce and a report row for one that is.\n *\n * The body is read only after a cheaper signal (class, sender, subject) has\n * matched — that ordering is what keeps a large-inbox scan affordable.\n */\nfunction bounceClassifierScript(): string {\n return `\n$bouncePhrases = ${psArray(BOUNCE_SUBJECT_PHRASES)}\n$daemonAddresses = ${psArray(BOUNCE_DAEMON_ADDRESSES)}\n$daemonNames = ${psArray(BOUNCE_DAEMON_NAMES)}\n$daemonSelf = ${psArray(DAEMON_SELF_ADDRESSES)}\n$ndrPrefix = ${psString(NDR_MESSAGE_CLASS_PREFIX)}\n$addressPattern = ${psString(BODY_ADDRESS_PATTERN)}\n$maxFailed = ${psInt(MAX_FAILED_RECIPIENTS)}\n$accountLower = $target.ToLower()\nfunction Get-BounceReport($item) {\n $class = 0\n try { $class = [int]$item.Class } catch {}\n if ($class -ne 43 -and $class -ne 46) { return $null }\n $subject = ''\n try { $subject = [string]$item.Subject } catch {}\n $senderName = ''\n try { $senderName = [string]$item.SenderName } catch {}\n $senderEmail = ''\n try { $senderEmail = [string]$item.SenderEmailAddress } catch {}\n $messageClass = ''\n try { $messageClass = [string]$item.MessageClass } catch {}\n $reason = ''\n if ($messageClass.StartsWith($ndrPrefix, [StringComparison]::OrdinalIgnoreCase)) { $reason = ${psString(BOUNCE_REASON.ndr)} }\n if (-not $reason) { foreach ($d in $daemonAddresses) { if ($senderEmail.ToLower().Contains($d)) { $reason = ${psString(BOUNCE_REASON.daemon)}; break } } }\n if (-not $reason) { foreach ($d in $daemonNames) { if ($senderName.ToLower().Contains($d)) { $reason = ${psString(BOUNCE_REASON.daemon)}; break } } }\n if (-not $reason) { foreach ($p in $bouncePhrases) { if ($subject.ToLower().Contains($p)) { $reason = \"${BOUNCE_REASON.subjectPhrase('$p')}\"; break } } }\n if (-not $reason) { return $null }\n $failedRecipients = @()\n $body = ''\n try { $body = [string]$item.Body } catch {}\n foreach ($m in [regex]::Matches($body, $addressPattern)) {\n $address = $m.Value.ToLower().TrimEnd('.')\n if ($address -eq $accountLower) { continue }\n $isDaemon = $false\n foreach ($d in $daemonSelf) { if ($address.Contains($d)) { $isDaemon = $true; break } }\n if ($isDaemon -or $failedRecipients -contains $address) { continue }\n $failedRecipients += $address\n if ($failedRecipients.Count -ge $maxFailed) { break }\n }\n $received = ''\n try { $received = $item.ReceivedTime.ToString('yyyy-MM-dd HH:mm') } catch {}\n return [PSCustomObject]@{\n entryId = [string]$item.EntryID\n subject = $subject.Trim()\n senderName = $senderName\n senderEmail = $senderEmail\n receivedTime = $received\n matchedReason = $reason\n failedRecipients = @($failedRecipients)\n }\n}\n`;\n}\n\n/**\n * Scan an account's Inbox for bounce-backs and, unless dry-running, move each to\n * Deleted Items (recoverable).\n *\n * Matched items are collected before the first Delete(): deleting shifts the\n * collection, and deleting mid-enumeration would skip items.\n */\nexport async function cleanUndeliverableEmails(request: CleanUndeliverableRequest): Promise<CleanUndeliverableResult> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${NAMED_STORE_PS}\n${bounceClassifierScript()}\n$dryRun = ${psBool(request.dryRun)}\n$inbox = $store.GetDefaultFolder(${psInt(FolderId.Inbox)})\n${cutoffScript(request.daysBack)}\n${itemsSinceScript('inbox', 'ReceivedTime')}\n$reports = @()\n$matchedItems = @()\n$count = $filtered.Count\nfor ($i = 1; $i -le $count; $i++) {\n $item = $null\n try { $item = $filtered.Item($i) } catch { continue }\n if ($item -eq $null) { continue }\n $received = $null\n try { $received = $item.ReceivedTime } catch {}\n if ($received -ne $null -and $received -lt $cutoff) { continue }\n $report = Get-BounceReport $item\n if ($report -eq $null) { continue }\n $reports += $report\n $matchedItems += $item\n}\n$deleted = 0\n$failed = @()\nif (-not $dryRun) {\n foreach ($m in $matchedItems) {\n $entryId = ''\n $subject = ''\n try { $entryId = [string]$m.EntryID } catch {}\n try { $subject = [string]$m.Subject } catch {}\n try { $m.Delete(); $deleted++ }\n catch { ${itemFailurePs('$entryId')} }\n }\n}\nConvertTo-Json -Depth 4 -InputObject ([PSCustomObject]@{\n deletedCount = $deleted\n matched = @($reports)\n failed = @($failed)\n})\n`, 'scan');\n const e = record(output);\n const matched = toArray(e.matched).map(row => {\n const m = record(row);\n return {\n entryId: str(m.entryId),\n subject: str(m.subject),\n senderName: str(m.senderName),\n senderEmail: str(m.senderEmail),\n receivedTime: str(m.receivedTime),\n matchedReason: str(m.matchedReason),\n failedRecipients: strList(m.failedRecipients),\n };\n });\n return {\n account: request.account,\n scannedDays: request.daysBack,\n dryRun: request.dryRun,\n matchedCount: matched.length,\n deletedCount: num(e.deletedCount),\n matched,\n failed: itemFailures(e.failed),\n };\n}\n\n/**\n * The deduplicated addresses bounce-backs report as failed, from the Inbox and,\n * optionally, Deleted Items — so it still works after the bounces were cleaned\n * away. Never deletes anything.\n */\nexport async function collectBouncedRecipients(request: CollectBouncesRequest): Promise<string[]> {\n const roles = request.includeDeletedItems ? [FolderId.Inbox, FolderId.DeletedItems] : [FolderId.Inbox];\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n${bounceClassifierScript()}\n${cutoffScript(request.daysBack)}\n$found = [ordered]@{}\nforeach ($role in @(${roles.map(psInt).join(', ')})) {\n $scanFolder = $null\n try { $scanFolder = $store.GetDefaultFolder($role) } catch {}\n if ($scanFolder -eq $null) { continue }\n ${itemsSinceScript('scanFolder', 'ReceivedTime').trim()}\n $count = $filtered.Count\n for ($i = 1; $i -le $count; $i++) {\n $item = $null\n try { $item = $filtered.Item($i) } catch { continue }\n if ($item -eq $null) { continue }\n $received = $null\n try { $received = $item.ReceivedTime } catch {}\n if ($received -ne $null -and $received -lt $cutoff) { continue }\n $report = Get-BounceReport $item\n if ($report -eq $null) { continue }\n foreach ($address in $report.failedRecipients) { $found[$address] = $true }\n }\n}\nConvertTo-Json -Compress -InputObject @($found.Keys)\n`, 'scan');\n return strList(output);\n}\n\n/**\n * Sent Items within the window, each message with the full SMTP address set it\n * went to (To + CC + BCC), newest first.\n */\nexport async function readSentRecipientGroups(request: SentGroupsRequest): Promise<SentRecipientGroup[]> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n$sentFolder = $store.GetDefaultFolder(${psInt(FolderId.SentMail)})\n${cutoffScript(request.daysBack)}\n${itemsSinceScript('sentFolder', 'SentOn')}\n$filtered.Sort('[SentOn]', $true)\n$limit = ${psInt(request.limit)}\n$rows = @()\n$count = $filtered.Count\nfor ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {\n $m = $null\n try { $m = $filtered.Item($i) } catch { continue }\n if ($m -eq $null) { continue }\n $sentOn = $null\n try { $sentOn = $m.SentOn } catch {}\n if ($sentOn -ne $null -and $sentOn -lt $cutoff) { break }\n $class = 0\n try { $class = [int]$m.Class } catch {}\n if ($class -ne 43) { continue }\n $addresses = @()\n try {\n foreach ($r in $m.Recipients) {\n $address = ''\n try { $address = [string]$r.Address } catch {}\n # An internal recipient resolves to an Exchange DN; recover its SMTP address.\n if ($address -notlike '*@*') {\n try { $address = [string]$r.AddressEntry.GetExchangeUser().PrimarySmtpAddress } catch {}\n }\n if ($address -like '*@*') { $addresses += $address.ToLower() }\n }\n } catch {}\n if ($addresses.Count -eq 0) { continue }\n $rows += [PSCustomObject]@{\n entryId = [string]$m.EntryID\n subject = ([string]$m.Subject).Trim()\n sentOn = if ($sentOn -ne $null) { $sentOn.ToString('yyyy-MM-dd HH:mm') } else { '' }\n recipients = @($addresses)\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 subject: str(e.subject),\n sentOn: str(e.sentOn),\n recipients: strList(e.recipients),\n };\n });\n}\n"]}
@@ -6,7 +6,11 @@ import type { DeleteMailResult, PurgeDeletedItemsResult } from '../types';
6
6
  *
7
7
  * This is the one operation that can reach received mail, and an entry id is an
8
8
  * unsafe key for it: a stale or wrong id can resolve to a DIFFERENT message.
9
- * Three things hold the line:
9
+ * Four things hold the line:
10
+ * - An id that resolves outside this account's store is refused. The bare
11
+ * lookup falls back to the default store, and the Inbox/Sent check below
12
+ * only knows this account's folders, so another mailbox's received mail
13
+ * would otherwise pass it.
10
14
  * - Inbox and Sent Items are refused, INCLUDING their subfolders (a filed
11
15
  * folder is still received mail), unless `allowProtected`.
12
16
  * - `dryRun` resolves and reports without deleting, so the exact subjects and
@@ -9,14 +9,17 @@ const json_1 = require("../shared/json");
9
9
  const mail_1 = require("../mail");
10
10
  /** Folders whose contents are received or sent mail rather than working state. */
11
11
  const PROTECTED_FOLDER_IDS = [mail_1.FolderId.Inbox, mail_1.FolderId.SentMail];
12
- const OUTCOME_STATUSES = ['deleted', 'would-delete', 'refused', 'failed'];
13
12
  /**
14
13
  * Delete mail by entry id from anywhere in the account. Delete() moves each item
15
14
  * to Deleted Items, so this is recoverable — purgeDeletedItems is what destroys.
16
15
  *
17
16
  * This is the one operation that can reach received mail, and an entry id is an
18
17
  * unsafe key for it: a stale or wrong id can resolve to a DIFFERENT message.
19
- * Three things hold the line:
18
+ * Four things hold the line:
19
+ * - An id that resolves outside this account's store is refused. The bare
20
+ * lookup falls back to the default store, and the Inbox/Sent check below
21
+ * only knows this account's folders, so another mailbox's received mail
22
+ * would otherwise pass it.
20
23
  * - Inbox and Sent Items are refused, INCLUDING their subfolders (a filed
21
24
  * folder is still received mail), unless `allowProtected`.
22
25
  * - `dryRun` resolves and reports without deleting, so the exact subjects and
@@ -46,6 +49,14 @@ function Test-UnderProtectedFolder($folder) {
46
49
  }
47
50
  return $false
48
51
  }
52
+ function Test-InAccountStore($folder) {
53
+ $folderStoreId = $null
54
+ try { $folderStoreId = [string]$folder.StoreID } catch {}
55
+ if (-not $folderStoreId) { return $false }
56
+ if ($folderStoreId -ieq $storeId) { return $true }
57
+ # One store can be named by two id strings; MAPI decides whether they match.
58
+ try { return [bool]$ns.CompareEntryIDs($folderStoreId, $storeId) } catch { return $false }
59
+ }
49
60
  $items = @()
50
61
  foreach ($id in ${(0, run_1.psArray)(request.entryIds)}) {
51
62
  $subject = ''
@@ -60,6 +71,7 @@ foreach ($id in ${(0, run_1.psArray)(request.entryIds)}) {
60
71
  try { $parent = $it.Parent } catch {}
61
72
  if ($parent -eq $null) { throw "item has no parent folder - refusing to delete" }
62
73
  try { $folderPath = [string]$parent.FolderPath } catch {}
74
+ if (-not (Test-InAccountStore $parent)) { throw "item is in another mailbox, not this account - refusing to delete" }
63
75
  if ((Test-UnderProtectedFolder $parent) -and -not $allowProtected) {
64
76
  $items += [PSCustomObject]@{ entryId = $id; subject = $subject; folderPath = $folderPath; status = 'refused'; reason = $protectedReason }
65
77
  } elseif ($dryRun) {
@@ -76,22 +88,15 @@ ConvertTo-Json -Depth 3 -InputObject @($items)
76
88
  `, 'scan');
77
89
  const items = (0, json_1.toArray)(output).map(row => {
78
90
  const e = (0, json_1.record)(row);
79
- const status = OUTCOME_STATUSES.find(s => s === (0, json_1.str)(e.status)) ?? 'failed';
80
91
  return {
81
92
  entryId: (0, json_1.str)(e.entryId),
82
93
  subject: (0, json_1.str)(e.subject),
83
94
  folderPath: (0, json_1.str)(e.folderPath),
84
- status,
95
+ status: (0, mail_1.deleteStatus)((0, json_1.str)(e.status)),
85
96
  reason: (0, json_1.str)(e.reason),
86
97
  };
87
98
  });
88
- return {
89
- dryRun: request.dryRun,
90
- deleted: items.filter(i => i.status === 'deleted').length,
91
- refused: items.filter(i => i.status === 'refused').length,
92
- failed: items.filter(i => i.status === 'failed').length,
93
- items,
94
- };
99
+ return (0, mail_1.deleteMailResult)(request.dryRun, items);
95
100
  }
96
101
  /**
97
102
  * Permanently remove items from the account's Deleted Items. The ONE irreversible
@@ -1 +1 @@
1
- {"version":3,"file":"cleanup.js","sourceRoot":"","sources":["../../src/windows/cleanup.ts"],"names":[],"mappings":"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","sourcesContent":["// Deleting mail, and emptying what was already deleted.\nimport { psArray, psBool, psInt, psString, runPowerShellJson } from './run';\nimport { accountScript, cutoffScript, DELIVERY_STORE_PS } from './scripts';\nimport { num, record, str, toArray } from '../shared/json';\nimport { FolderId, PROTECTED_MAIL_REASON } from '../mail';\nimport type { DeleteEmailsRequest, PurgeRequest } from '../backend';\nimport type { DeleteMailOutcome, DeleteMailResult, PurgeDeletedItemsResult } from '../types';\n\n/** Folders whose contents are received or sent mail rather than working state. */\nconst PROTECTED_FOLDER_IDS = [FolderId.Inbox, FolderId.SentMail];\n\nconst OUTCOME_STATUSES: readonly DeleteMailOutcome['status'][] = ['deleted', 'would-delete', 'refused', 'failed'];\n\n/**\n * Delete mail by entry id from anywhere in the account. Delete() moves each item\n * to Deleted Items, so this is recoverable — purgeDeletedItems is what destroys.\n *\n * This is the one operation that can reach received mail, and an entry id is an\n * unsafe key for it: a stale or wrong id can resolve to a DIFFERENT message.\n * Three things hold the line:\n * - Inbox and Sent Items are refused, INCLUDING their subfolders (a filed\n * folder is still received mail), unless `allowProtected`.\n * - `dryRun` resolves and reports without deleting, so the exact subjects and\n * folders can be checked before anything happens.\n * - Every outcome echoes the subject and folder of the item actually resolved.\n */\nexport async function deleteOutlookEmails(request: DeleteEmailsRequest): Promise<DeleteMailResult> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n$allowProtected = ${psBool(request.allowProtected)}\n$dryRun = ${psBool(request.dryRun)}\n$protectedReason = ${psString(PROTECTED_MAIL_REASON)}\n$protectedIds = @{}\nforeach ($role in @(${PROTECTED_FOLDER_IDS.map(psInt).join(', ')})) {\n try { $protectedIds[[string]$store.GetDefaultFolder($role).EntryID] = $true } catch {}\n}\nfunction Test-UnderProtectedFolder($folder) {\n $current = $folder\n for ($guard = 0; $current -ne $null -and $guard -lt 25; $guard++) {\n $folderId = $null\n try { $folderId = [string]$current.EntryID } catch {}\n if (-not $folderId) { return $false }\n if ($protectedIds.ContainsKey($folderId)) { return $true }\n $parent = $null\n try { $parent = $current.Parent } catch {}\n $current = $parent\n }\n return $false\n}\n$items = @()\nforeach ($id in ${psArray(request.entryIds)}) {\n $subject = ''\n $folderPath = ''\n try {\n $it = $null\n try { $it = $ns.GetItemFromID($id, $store.StoreID) } catch { $it = $null }\n if ($it -eq $null) { try { $it = $ns.GetItemFromID($id) } catch { $it = $null } }\n if ($it -eq $null) { throw \"no item with that entry id in this account\" }\n try { $subject = [string]$it.Subject } catch {}\n $parent = $null\n try { $parent = $it.Parent } catch {}\n if ($parent -eq $null) { throw \"item has no parent folder - refusing to delete\" }\n try { $folderPath = [string]$parent.FolderPath } catch {}\n if ((Test-UnderProtectedFolder $parent) -and -not $allowProtected) {\n $items += [PSCustomObject]@{ entryId = $id; subject = $subject; folderPath = $folderPath; status = 'refused'; reason = $protectedReason }\n } elseif ($dryRun) {\n $items += [PSCustomObject]@{ entryId = $id; subject = $subject; folderPath = $folderPath; status = 'would-delete'; reason = '' }\n } else {\n $it.Delete()\n $items += [PSCustomObject]@{ entryId = $id; subject = $subject; folderPath = $folderPath; status = 'deleted'; reason = '' }\n }\n } catch {\n $items += [PSCustomObject]@{ entryId = $id; subject = $subject; folderPath = $folderPath; status = 'failed'; reason = $_.Exception.Message }\n }\n}\nConvertTo-Json -Depth 3 -InputObject @($items)\n`, 'scan');\n const items: DeleteMailOutcome[] = toArray(output).map(row => {\n const e = record(row);\n const status = OUTCOME_STATUSES.find(s => s === str(e.status)) ?? 'failed';\n return {\n entryId: str(e.entryId),\n subject: str(e.subject),\n folderPath: str(e.folderPath),\n status,\n reason: str(e.reason),\n };\n });\n return {\n dryRun: request.dryRun,\n deleted: items.filter(i => i.status === 'deleted').length,\n refused: items.filter(i => i.status === 'refused').length,\n failed: items.filter(i => i.status === 'failed').length,\n items,\n };\n}\n\n/**\n * Permanently remove items from the account's Deleted Items. The ONE irreversible\n * operation here, which is why it is folder-scoped rather than keyed on ids: it\n * can only ever destroy what was already thrown away.\n *\n * Walks backwards, since deleting shifts the collection and a forward walk would\n * skip every other item.\n */\nexport async function purgeDeletedItems(request: PurgeRequest): Promise<PurgeDeletedItemsResult> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n$folder = $store.GetDefaultFolder(${psInt(FolderId.DeletedItems)})\n$olderThanDays = ${psInt(request.olderThanDays)}\n$dryRun = ${psBool(request.dryRun)}\n${cutoffScript(request.olderThanDays)}\n$matched = 0\n$purged = 0\n$kept = 0\n$failed = 0\n# Bound once: each $folder.Items is a COM call that builds a fresh collection.\n$folderItems = $folder.Items\nfor ($i = $folderItems.Count; $i -ge 1; $i--) {\n $it = $null\n try { $it = $folderItems.Item($i) } catch { continue }\n $stamp = $null\n foreach ($property in @('ReceivedTime', 'LastModificationTime', 'CreationTime')) {\n try { $stamp = $it.$property; if ($stamp -ne $null) { break } } catch {}\n }\n if ($olderThanDays -gt 0 -and $stamp -ne $null -and $stamp -gt $cutoff) { $kept++; continue }\n $matched++\n if ($dryRun) { continue }\n try { $it.Delete(); $purged++ } catch { $failed++ }\n}\nConvertTo-Json -Compress -InputObject ([PSCustomObject]@{\n folderPath = [string]$folder.FolderPath\n matched = $matched\n purged = $purged\n kept = $kept\n failed = $failed\n})\n`, 'purge');\n const e = record(output);\n return {\n folderPath: str(e.folderPath),\n dryRun: request.dryRun,\n matched: num(e.matched),\n purged: num(e.purged),\n kept: num(e.kept),\n failed: num(e.failed),\n };\n}\n"]}
1
+ {"version":3,"file":"cleanup.js","sourceRoot":"","sources":["../../src/windows/cleanup.ts"],"names":[],"mappings":";;AA4BA,kDAuEC;AAUD,8CA0CC;AAvJD,wDAAwD;AACxD,+BAA4E;AAC5E,uCAA2E;AAC3E,yCAA2D;AAC3D,kCAA0F;AAI1F,kFAAkF;AAClF,MAAM,oBAAoB,GAAG,CAAC,eAAQ,CAAC,KAAK,EAAE,eAAQ,CAAC,QAAQ,CAAC,CAAC;AAEjE;;;;;;;;;;;;;;;;GAgBG;AACI,KAAK,UAAU,mBAAmB,CAAC,OAA4B;IAClE,MAAM,MAAM,GAAG,MAAM,IAAA,uBAAiB,EAAC,GAAG,IAAA,uBAAa,EAAC,OAAO,CAAC,OAAO,CAAC;EAC1E,2BAAiB;oBACC,IAAA,YAAM,EAAC,OAAO,CAAC,cAAc,CAAC;YACtC,IAAA,YAAM,EAAC,OAAO,CAAC,MAAM,CAAC;qBACb,IAAA,cAAQ,EAAC,4BAAqB,CAAC;;sBAE9B,oBAAoB,CAAC,GAAG,CAAC,WAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;kBAyB9C,IAAA,aAAO,EAAC,OAAO,CAAC,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2B1C,EAAE,MAAM,CAAC,CAAC;IACP,MAAM,KAAK,GAAwB,IAAA,cAAO,EAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;QACzD,MAAM,CAAC,GAAG,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC;QACtB,OAAO;YACH,OAAO,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,OAAO,CAAC;YACvB,OAAO,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,OAAO,CAAC;YACvB,UAAU,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,UAAU,CAAC;YAC7B,MAAM,EAAE,IAAA,mBAAY,EAAC,IAAA,UAAG,EAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YACnC,MAAM,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,MAAM,CAAC;SACxB,CAAC;IACN,CAAC,CAAC,CAAC;IACH,OAAO,IAAA,uBAAgB,EAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;;GAOG;AACI,KAAK,UAAU,iBAAiB,CAAC,OAAqB;IACzD,MAAM,MAAM,GAAG,MAAM,IAAA,uBAAiB,EAAC,GAAG,IAAA,uBAAa,EAAC,OAAO,CAAC,OAAO,CAAC;EAC1E,2BAAiB;oCACiB,IAAA,WAAK,EAAC,eAAQ,CAAC,YAAY,CAAC;mBAC7C,IAAA,WAAK,EAAC,OAAO,CAAC,aAAa,CAAC;YACnC,IAAA,YAAM,EAAC,OAAO,CAAC,MAAM,CAAC;EAChC,IAAA,sBAAY,EAAC,OAAO,CAAC,aAAa,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BpC,EAAE,OAAO,CAAC,CAAC;IACR,MAAM,CAAC,GAAG,IAAA,aAAM,EAAC,MAAM,CAAC,CAAC;IACzB,OAAO;QACH,UAAU,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,UAAU,CAAC;QAC7B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,OAAO,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,OAAO,CAAC;QACvB,MAAM,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,MAAM,CAAC;QACrB,IAAI,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,IAAI,CAAC;QACjB,MAAM,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,MAAM,CAAC;KACxB,CAAC;AACN,CAAC","sourcesContent":["// Deleting mail, and emptying what was already deleted.\nimport { psArray, psBool, psInt, psString, runPowerShellJson } from './run';\nimport { accountScript, cutoffScript, DELIVERY_STORE_PS } from './scripts';\nimport { num, record, str, toArray } from '../shared/json';\nimport { deleteMailResult, deleteStatus, FolderId, PROTECTED_MAIL_REASON } from '../mail';\nimport type { DeleteEmailsRequest, PurgeRequest } from '../backend';\nimport type { DeleteMailOutcome, DeleteMailResult, PurgeDeletedItemsResult } from '../types';\n\n/** Folders whose contents are received or sent mail rather than working state. */\nconst PROTECTED_FOLDER_IDS = [FolderId.Inbox, FolderId.SentMail];\n\n/**\n * Delete mail by entry id from anywhere in the account. Delete() moves each item\n * to Deleted Items, so this is recoverable — purgeDeletedItems is what destroys.\n *\n * This is the one operation that can reach received mail, and an entry id is an\n * unsafe key for it: a stale or wrong id can resolve to a DIFFERENT message.\n * Four things hold the line:\n * - An id that resolves outside this account's store is refused. The bare\n * lookup falls back to the default store, and the Inbox/Sent check below\n * only knows this account's folders, so another mailbox's received mail\n * would otherwise pass it.\n * - Inbox and Sent Items are refused, INCLUDING their subfolders (a filed\n * folder is still received mail), unless `allowProtected`.\n * - `dryRun` resolves and reports without deleting, so the exact subjects and\n * folders can be checked before anything happens.\n * - Every outcome echoes the subject and folder of the item actually resolved.\n */\nexport async function deleteOutlookEmails(request: DeleteEmailsRequest): Promise<DeleteMailResult> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n$allowProtected = ${psBool(request.allowProtected)}\n$dryRun = ${psBool(request.dryRun)}\n$protectedReason = ${psString(PROTECTED_MAIL_REASON)}\n$protectedIds = @{}\nforeach ($role in @(${PROTECTED_FOLDER_IDS.map(psInt).join(', ')})) {\n try { $protectedIds[[string]$store.GetDefaultFolder($role).EntryID] = $true } catch {}\n}\nfunction Test-UnderProtectedFolder($folder) {\n $current = $folder\n for ($guard = 0; $current -ne $null -and $guard -lt 25; $guard++) {\n $folderId = $null\n try { $folderId = [string]$current.EntryID } catch {}\n if (-not $folderId) { return $false }\n if ($protectedIds.ContainsKey($folderId)) { return $true }\n $parent = $null\n try { $parent = $current.Parent } catch {}\n $current = $parent\n }\n return $false\n}\nfunction Test-InAccountStore($folder) {\n $folderStoreId = $null\n try { $folderStoreId = [string]$folder.StoreID } catch {}\n if (-not $folderStoreId) { return $false }\n if ($folderStoreId -ieq $storeId) { return $true }\n # One store can be named by two id strings; MAPI decides whether they match.\n try { return [bool]$ns.CompareEntryIDs($folderStoreId, $storeId) } catch { return $false }\n}\n$items = @()\nforeach ($id in ${psArray(request.entryIds)}) {\n $subject = ''\n $folderPath = ''\n try {\n $it = $null\n try { $it = $ns.GetItemFromID($id, $store.StoreID) } catch { $it = $null }\n if ($it -eq $null) { try { $it = $ns.GetItemFromID($id) } catch { $it = $null } }\n if ($it -eq $null) { throw \"no item with that entry id in this account\" }\n try { $subject = [string]$it.Subject } catch {}\n $parent = $null\n try { $parent = $it.Parent } catch {}\n if ($parent -eq $null) { throw \"item has no parent folder - refusing to delete\" }\n try { $folderPath = [string]$parent.FolderPath } catch {}\n if (-not (Test-InAccountStore $parent)) { throw \"item is in another mailbox, not this account - refusing to delete\" }\n if ((Test-UnderProtectedFolder $parent) -and -not $allowProtected) {\n $items += [PSCustomObject]@{ entryId = $id; subject = $subject; folderPath = $folderPath; status = 'refused'; reason = $protectedReason }\n } elseif ($dryRun) {\n $items += [PSCustomObject]@{ entryId = $id; subject = $subject; folderPath = $folderPath; status = 'would-delete'; reason = '' }\n } else {\n $it.Delete()\n $items += [PSCustomObject]@{ entryId = $id; subject = $subject; folderPath = $folderPath; status = 'deleted'; reason = '' }\n }\n } catch {\n $items += [PSCustomObject]@{ entryId = $id; subject = $subject; folderPath = $folderPath; status = 'failed'; reason = $_.Exception.Message }\n }\n}\nConvertTo-Json -Depth 3 -InputObject @($items)\n`, 'scan');\n const items: DeleteMailOutcome[] = toArray(output).map(row => {\n const e = record(row);\n return {\n entryId: str(e.entryId),\n subject: str(e.subject),\n folderPath: str(e.folderPath),\n status: deleteStatus(str(e.status)),\n reason: str(e.reason),\n };\n });\n return deleteMailResult(request.dryRun, items);\n}\n\n/**\n * Permanently remove items from the account's Deleted Items. The ONE irreversible\n * operation here, which is why it is folder-scoped rather than keyed on ids: it\n * can only ever destroy what was already thrown away.\n *\n * Walks backwards, since deleting shifts the collection and a forward walk would\n * skip every other item.\n */\nexport async function purgeDeletedItems(request: PurgeRequest): Promise<PurgeDeletedItemsResult> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n$folder = $store.GetDefaultFolder(${psInt(FolderId.DeletedItems)})\n$olderThanDays = ${psInt(request.olderThanDays)}\n$dryRun = ${psBool(request.dryRun)}\n${cutoffScript(request.olderThanDays)}\n$matched = 0\n$purged = 0\n$kept = 0\n$failed = 0\n# Bound once: each $folder.Items is a COM call that builds a fresh collection.\n$folderItems = $folder.Items\nfor ($i = $folderItems.Count; $i -ge 1; $i--) {\n $it = $null\n try { $it = $folderItems.Item($i) } catch { continue }\n $stamp = $null\n foreach ($property in @('ReceivedTime', 'LastModificationTime', 'CreationTime')) {\n try { $stamp = $it.$property; if ($stamp -ne $null) { break } } catch {}\n }\n if ($olderThanDays -gt 0 -and $stamp -ne $null -and $stamp -gt $cutoff) { $kept++; continue }\n $matched++\n if ($dryRun) { continue }\n try { $it.Delete(); $purged++ } catch { $failed++ }\n}\nConvertTo-Json -Compress -InputObject ([PSCustomObject]@{\n folderPath = [string]$folder.FolderPath\n matched = $matched\n purged = $purged\n kept = $kept\n failed = $failed\n})\n`, 'purge');\n const e = record(output);\n return {\n folderPath: str(e.folderPath),\n dryRun: request.dryRun,\n matched: num(e.matched),\n purged: num(e.purged),\n kept: num(e.kept),\n failed: num(e.failed),\n };\n}\n"]}
@@ -2,7 +2,7 @@ import type { AccountRequest, EntryIdsRequest, ListDraftsRequest } from '../back
2
2
  import type { DeleteDraftsResult, ListDraftsResult, SendDraftsResult } from '../types';
3
3
  /** The account's drafts, newest first. Bodies are previewed, never returned whole. */
4
4
  export declare function listOutlookDrafts(request: ListDraftsRequest): Promise<ListDraftsResult>;
5
- /** Send the named drafts, each proved to be this account's first. */
5
+ /** Send the named drafts. */
6
6
  export declare function sendDrafts(request: EntryIdsRequest): Promise<SendDraftsResult>;
7
7
  /**
8
8
  * Send every draft belonging to the account.
@@ -13,9 +13,9 @@ export declare function sendDrafts(request: EntryIdsRequest): Promise<SendDrafts
13
13
  */
14
14
  export declare function sendAllDrafts(request: AccountRequest): Promise<SendDraftsResult>;
15
15
  /**
16
- * Delete the named drafts, each proved to be this account's first. Outlook's
17
- * Delete() moves an item to Deleted Items, so a mistake stays recoverable.
18
- * Without the proof this would be a general "delete any email by id" call,
16
+ * Delete the named drafts. Outlook's Delete() moves an item to Deleted Items,
17
+ * so a mistake stays recoverable. Without the proof that each is this
18
+ * account's draft, this would be a general "delete any email by id" call,
19
19
  * which is not what it is for.
20
20
  */
21
21
  export declare function deleteOutlookDrafts(request: EntryIdsRequest): Promise<DeleteDraftsResult>;
@@ -143,26 +143,31 @@ ConvertTo-Json -Depth 4 -InputObject ([PSCustomObject]@{
143
143
  }),
144
144
  };
145
145
  }
146
- /** Send the named drafts, each proved to be this account's first. */
147
- async function sendDrafts(request) {
146
+ /** Run `verb` over the named drafts of the account, each proved to be its own first. */
147
+ async function actOnDrafts(request, verb) {
148
148
  const output = await (0, run_1.runPowerShellJson)(`${draftsScript(request.account)}
149
- $sent = 0
149
+ $done = 0
150
150
  $failed = @()
151
151
  foreach ($id in ${(0, run_1.psArray)(request.entryIds)}) {
152
152
  $subject = ''
153
153
  try {
154
- $draft = Resolve-OwnDraft $id 'send'
154
+ $draft = Resolve-OwnDraft $id '${verb}'
155
155
  try { $subject = [string]$draft.Subject } catch {}
156
- $draft.Send()
157
- $sent++
156
+ $draft.${verb === 'send' ? 'Send' : 'Delete'}()
157
+ $done++
158
158
  } catch {
159
- $failed += [PSCustomObject]@{ entryId = $id; subject = $subject; error = $_.Exception.Message }
159
+ ${(0, json_1.itemFailurePs)('$id')}
160
160
  }
161
161
  }
162
- ConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{ sent = $sent; failed = @($failed) })
162
+ ConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{ done = $done; failed = @($failed) })
163
163
  `, 'scan');
164
164
  const e = (0, json_1.record)(output);
165
- return { sent: (0, json_1.num)(e.sent), failed: (0, json_1.itemFailures)(e.failed) };
165
+ return { done: (0, json_1.num)(e.done), failed: (0, json_1.itemFailures)(e.failed) };
166
+ }
167
+ /** Send the named drafts. */
168
+ async function sendDrafts(request) {
169
+ const { done, failed } = await actOnDrafts(request, 'send');
170
+ return { sent: done, failed };
166
171
  }
167
172
  /**
168
173
  * Send every draft belonging to the account.
@@ -190,7 +195,7 @@ foreach ($draft in $drafts) {
190
195
  $draft.Send()
191
196
  $sent++
192
197
  } catch {
193
- $failed += [PSCustomObject]@{ entryId = $entryId; subject = $subject; error = $_.Exception.Message }
198
+ ${(0, json_1.itemFailurePs)('$entryId')}
194
199
  }
195
200
  }
196
201
  ConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{ sent = $sent; failed = @($failed) })
@@ -199,29 +204,13 @@ ConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{ sent = $sent;
199
204
  return { sent: (0, json_1.num)(e.sent), failed: (0, json_1.itemFailures)(e.failed) };
200
205
  }
201
206
  /**
202
- * Delete the named drafts, each proved to be this account's first. Outlook's
203
- * Delete() moves an item to Deleted Items, so a mistake stays recoverable.
204
- * Without the proof this would be a general "delete any email by id" call,
207
+ * Delete the named drafts. Outlook's Delete() moves an item to Deleted Items,
208
+ * so a mistake stays recoverable. Without the proof that each is this
209
+ * account's draft, this would be a general "delete any email by id" call,
205
210
  * which is not what it is for.
206
211
  */
207
212
  async function deleteOutlookDrafts(request) {
208
- const output = await (0, run_1.runPowerShellJson)(`${draftsScript(request.account)}
209
- $deleted = 0
210
- $failed = @()
211
- foreach ($id in ${(0, run_1.psArray)(request.entryIds)}) {
212
- $subject = ''
213
- try {
214
- $draft = Resolve-OwnDraft $id 'delete'
215
- try { $subject = [string]$draft.Subject } catch {}
216
- $draft.Delete()
217
- $deleted++
218
- } catch {
219
- $failed += [PSCustomObject]@{ entryId = $id; subject = $subject; error = $_.Exception.Message }
220
- }
221
- }
222
- ConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{ deleted = $deleted; failed = @($failed) })
223
- `, 'scan');
224
- const e = (0, json_1.record)(output);
225
- return { deleted: (0, json_1.num)(e.deleted), failed: (0, json_1.itemFailures)(e.failed) };
213
+ const { done, failed } = await actOnDrafts(request, 'delete');
214
+ return { deleted: done, failed };
226
215
  }
227
216
  //# sourceMappingURL=drafts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"drafts.js","sourceRoot":"","sources":["../../src/windows/drafts.ts"],"names":[],"mappings":"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","sourcesContent":["// Drafts: which ones belong to an account, and listing, sending or deleting them.\nimport { psArray, psInt, runPowerShellJson } from './run';\nimport { accountScript, DELIVERY_STORE_PS } from './scripts';\nimport { bool, itemFailures, num, record, str, strList, toArray } from '../shared/json';\nimport { FolderId } from '../mail';\nimport type { AccountRequest, EntryIdsRequest, ListDraftsRequest } from '../backend';\nimport type { DeleteDraftsResult, ListDraftsResult, SendDraftsResult } from '../types';\n\n/**\n * Resolve the Drafts folders belonging to one account into `$scan`, and define\n * `Test-DraftMatches` over them. Emitted into every drafts script, so listing,\n * sending and deleting can never disagree about which drafts are this account's.\n *\n * Two Drafts folders can hold one account's drafts, so both are scanned:\n * - the account's OWN store's Drafts, where a draft composed in that mailbox\n * lands. A draft there with no explicit sending account still belongs to it.\n * - the DEFAULT store's Drafts, where a program's `Save()` files a draft\n * whatever account it sends from. Only drafts stamped with this account\n * match there; an unstamped one belongs to the default account.\n * When the two are the same folder it is scanned once, under the first rule.\n *\n * Non-mail items (meeting requests, reports) never match.\n */\nconst DRAFTS_SCAN_PS = `\n$scan = @()\n$seenFolders = @{}\n$homeDrafts = $null\ntry { $homeDrafts = $store.GetDefaultFolder(${psInt(FolderId.Drafts)}) } catch {}\nif ($homeDrafts -ne $null) {\n $seenFolders[\"$($homeDrafts.StoreID)|$($homeDrafts.EntryID)\"] = $true\n $scan += [PSCustomObject]@{ folder = $homeDrafts; includeUnstamped = $true }\n}\n$defaultDrafts = $null\ntry { $defaultDrafts = $ns.GetDefaultFolder(${psInt(FolderId.Drafts)}) } catch {}\nif ($defaultDrafts -ne $null -and -not $seenFolders.ContainsKey(\"$($defaultDrafts.StoreID)|$($defaultDrafts.EntryID)\")) {\n $scan += [PSCustomObject]@{ folder = $defaultDrafts; includeUnstamped = $false }\n}\nfunction Test-DraftMatches($item, [bool]$includeUnstamped) {\n if ($item.Class -ne 43) { return $false } # olMail only\n $sender = $null\n try { $sender = $item.SendUsingAccount } catch {}\n if ($sender -eq $null) { return $includeUnstamped }\n return ([string]$sender.SmtpAddress -ieq $target)\n}\n# An id is acted on only once it is PROVED to be one of this account's drafts:\n# resolved in one of the scanned folders' stores, sitting in that folder, and\n# bound to this account (or unbound, in its own store).\n$draftFolderRules = @{}\nforeach ($entry in $scan) { $draftFolderRules[[string]$entry.folder.EntryID] = $entry.includeUnstamped }\nfunction Resolve-OwnDraft([string]$id, [string]$verb) {\n $draft = $null\n foreach ($entry in $scan) {\n try { $draft = $ns.GetItemFromID($id, $entry.folder.StoreID) } catch { $draft = $null }\n if ($draft -ne $null) { break }\n }\n if ($draft -eq $null) { throw \"no such item in this account's Drafts folders\" }\n $parentId = ''\n try { $parentId = [string]$draft.Parent.EntryID } catch {}\n if (-not $draftFolderRules.ContainsKey($parentId)) { throw \"item is not in this account's Drafts folder - refusing to $verb\" }\n if (-not (Test-DraftMatches $draft $draftFolderRules[$parentId])) { throw \"draft is not bound to $target - refusing to $verb\" }\n return $draft\n}\n`;\n\nfunction draftsScript(account: string): string {\n return accountScript(account) + DELIVERY_STORE_PS + DRAFTS_SCAN_PS;\n}\n\n/** The account's drafts, newest first. Bodies are previewed, never returned whole. */\nexport async function listOutlookDrafts(request: ListDraftsRequest): Promise<ListDraftsResult> {\n const output = await runPowerShellJson(`${draftsScript(request.account)}\n$previewChars = ${psInt(request.previewChars)}\n$limit = ${psInt(request.limit)}\n$rows = @()\n$folderPaths = @()\nforeach ($entry in $scan) {\n $folderPath = ''\n try { $folderPath = [string]$entry.folder.FolderPath } catch {}\n $folderPaths += $folderPath\n foreach ($it in $entry.folder.Items) {\n if (-not (Test-DraftMatches $it $entry.includeUnstamped)) { continue }\n $addresses = @()\n try {\n foreach ($r in $it.Recipients) {\n $address = ''\n try { $address = [string]$r.Address } catch {}\n # An unresolved Exchange entry has a DN for an address; its name reads better.\n if ($address.StartsWith('/')) { try { $address = [string]$r.Name } catch {} }\n if ($address) { $addresses += $address }\n }\n } catch {}\n $preview = ''\n if ($previewChars -gt 0) {\n try { $preview = ([string]$it.Body -replace '\\\\s+', ' ').Trim() } catch {}\n if ($preview.Length -gt $previewChars) { $preview = $preview.Substring(0, $previewChars) }\n }\n $modified = ''\n try { $modified = $it.LastModificationTime.ToString('yyyy-MM-dd HH:mm') } catch {}\n $hasAttachments = $false\n try { $hasAttachments = ($it.Attachments.Count -gt 0) } catch {}\n $rows += [PSCustomObject]@{\n entryId = [string]$it.EntryID\n subject = [string]$it.Subject\n to = [string]$it.To\n toEmails = @($addresses)\n bodyPreview = $preview\n hasAttachments = $hasAttachments\n lastModified = $modified\n folderPath = $folderPath\n }\n }\n}\n$sorted = @($rows | Sort-Object -Property lastModified -Descending)\n$total = $sorted.Count\nif ($total -gt $limit) { $sorted = @($sorted[0..($limit - 1)]) }\nConvertTo-Json -Depth 4 -InputObject ([PSCustomObject]@{\n account = $target\n foldersScanned = @($folderPaths)\n count = $total\n truncated = ($total -gt $limit)\n drafts = @($sorted)\n})\n`, 'scan');\n const e = record(output);\n return {\n account: str(e.account) || request.account,\n foldersScanned: toArray(e.foldersScanned).map(str),\n count: num(e.count),\n truncated: bool(e.truncated),\n drafts: toArray(e.drafts).map(row => {\n const d = record(row);\n return {\n entryId: str(d.entryId),\n subject: str(d.subject),\n to: str(d.to),\n toEmails: strList(d.toEmails),\n bodyPreview: str(d.bodyPreview),\n hasAttachments: bool(d.hasAttachments),\n lastModified: str(d.lastModified),\n folderPath: str(d.folderPath),\n };\n }),\n };\n}\n\n/** Send the named drafts, each proved to be this account's first. */\nexport async function sendDrafts(request: EntryIdsRequest): Promise<SendDraftsResult> {\n const output = await runPowerShellJson(`${draftsScript(request.account)}\n$sent = 0\n$failed = @()\nforeach ($id in ${psArray(request.entryIds)}) {\n $subject = ''\n try {\n $draft = Resolve-OwnDraft $id 'send'\n try { $subject = [string]$draft.Subject } catch {}\n $draft.Send()\n $sent++\n } catch {\n $failed += [PSCustomObject]@{ entryId = $id; subject = $subject; error = $_.Exception.Message }\n }\n}\nConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{ sent = $sent; failed = @($failed) })\n`, 'scan');\n const e = record(output);\n return {sent: num(e.sent), failed: itemFailures(e.failed)};\n}\n\n/**\n * Send every draft belonging to the account.\n *\n * The matching items are snapshotted before the first send: sending moves an\n * item out of Drafts, and sending during a live enumeration would skip every\n * other one.\n */\nexport async function sendAllDrafts(request: AccountRequest): Promise<SendDraftsResult> {\n const output = await runPowerShellJson(`${draftsScript(request.account)}\n$drafts = @()\nforeach ($entry in $scan) {\n foreach ($it in $entry.folder.Items) {\n if (Test-DraftMatches $it $entry.includeUnstamped) { $drafts += $it }\n }\n}\n$sent = 0\n$failed = @()\nforeach ($draft in $drafts) {\n $entryId = ''\n $subject = ''\n try { $entryId = [string]$draft.EntryID } catch {}\n try { $subject = [string]$draft.Subject } catch {}\n try {\n $draft.Send()\n $sent++\n } catch {\n $failed += [PSCustomObject]@{ entryId = $entryId; subject = $subject; error = $_.Exception.Message }\n }\n}\nConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{ sent = $sent; failed = @($failed) })\n`, 'scan');\n const e = record(output);\n return {sent: num(e.sent), failed: itemFailures(e.failed)};\n}\n\n/**\n * Delete the named drafts, each proved to be this account's first. Outlook's\n * Delete() moves an item to Deleted Items, so a mistake stays recoverable.\n * Without the proof this would be a general \"delete any email by id\" call,\n * which is not what it is for.\n */\nexport async function deleteOutlookDrafts(request: EntryIdsRequest): Promise<DeleteDraftsResult> {\n const output = await runPowerShellJson(`${draftsScript(request.account)}\n$deleted = 0\n$failed = @()\nforeach ($id in ${psArray(request.entryIds)}) {\n $subject = ''\n try {\n $draft = Resolve-OwnDraft $id 'delete'\n try { $subject = [string]$draft.Subject } catch {}\n $draft.Delete()\n $deleted++\n } catch {\n $failed += [PSCustomObject]@{ entryId = $id; subject = $subject; error = $_.Exception.Message }\n }\n}\nConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{ deleted = $deleted; failed = @($failed) })\n`, 'scan');\n const e = record(output);\n return {deleted: num(e.deleted), failed: itemFailures(e.failed)};\n}\n"]}
1
+ {"version":3,"file":"drafts.js","sourceRoot":"","sources":["../../src/windows/drafts.ts"],"names":[],"mappings":"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","sourcesContent":["// Drafts: which ones belong to an account, and listing, sending or deleting them.\nimport { psArray, psInt, runPowerShellJson } from './run';\nimport { accountScript, DELIVERY_STORE_PS } from './scripts';\nimport { bool, itemFailurePs, itemFailures, num, record, str, strList, toArray } from '../shared/json';\nimport { FolderId } from '../mail';\nimport type { AccountRequest, EntryIdsRequest, ListDraftsRequest } from '../backend';\nimport type { DeleteDraftsResult, ItemFailure, ListDraftsResult, SendDraftsResult } from '../types';\n\n/**\n * Resolve the Drafts folders belonging to one account into `$scan`, and define\n * `Test-DraftMatches` over them. Emitted into every drafts script, so listing,\n * sending and deleting can never disagree about which drafts are this account's.\n *\n * Two Drafts folders can hold one account's drafts, so both are scanned:\n * - the account's OWN store's Drafts, where a draft composed in that mailbox\n * lands. A draft there with no explicit sending account still belongs to it.\n * - the DEFAULT store's Drafts, where a program's `Save()` files a draft\n * whatever account it sends from. Only drafts stamped with this account\n * match there; an unstamped one belongs to the default account.\n * When the two are the same folder it is scanned once, under the first rule.\n *\n * Non-mail items (meeting requests, reports) never match.\n */\nconst DRAFTS_SCAN_PS = `\n$scan = @()\n$seenFolders = @{}\n$homeDrafts = $null\ntry { $homeDrafts = $store.GetDefaultFolder(${psInt(FolderId.Drafts)}) } catch {}\nif ($homeDrafts -ne $null) {\n $seenFolders[\"$($homeDrafts.StoreID)|$($homeDrafts.EntryID)\"] = $true\n $scan += [PSCustomObject]@{ folder = $homeDrafts; includeUnstamped = $true }\n}\n$defaultDrafts = $null\ntry { $defaultDrafts = $ns.GetDefaultFolder(${psInt(FolderId.Drafts)}) } catch {}\nif ($defaultDrafts -ne $null -and -not $seenFolders.ContainsKey(\"$($defaultDrafts.StoreID)|$($defaultDrafts.EntryID)\")) {\n $scan += [PSCustomObject]@{ folder = $defaultDrafts; includeUnstamped = $false }\n}\nfunction Test-DraftMatches($item, [bool]$includeUnstamped) {\n if ($item.Class -ne 43) { return $false } # olMail only\n $sender = $null\n try { $sender = $item.SendUsingAccount } catch {}\n if ($sender -eq $null) { return $includeUnstamped }\n return ([string]$sender.SmtpAddress -ieq $target)\n}\n# An id is acted on only once it is PROVED to be one of this account's drafts:\n# resolved in one of the scanned folders' stores, sitting in that folder, and\n# bound to this account (or unbound, in its own store).\n$draftFolderRules = @{}\nforeach ($entry in $scan) { $draftFolderRules[[string]$entry.folder.EntryID] = $entry.includeUnstamped }\nfunction Resolve-OwnDraft([string]$id, [string]$verb) {\n $draft = $null\n foreach ($entry in $scan) {\n try { $draft = $ns.GetItemFromID($id, $entry.folder.StoreID) } catch { $draft = $null }\n if ($draft -ne $null) { break }\n }\n if ($draft -eq $null) { throw \"no such item in this account's Drafts folders\" }\n $parentId = ''\n try { $parentId = [string]$draft.Parent.EntryID } catch {}\n if (-not $draftFolderRules.ContainsKey($parentId)) { throw \"item is not in this account's Drafts folder - refusing to $verb\" }\n if (-not (Test-DraftMatches $draft $draftFolderRules[$parentId])) { throw \"draft is not bound to $target - refusing to $verb\" }\n return $draft\n}\n`;\n\nfunction draftsScript(account: string): string {\n return accountScript(account) + DELIVERY_STORE_PS + DRAFTS_SCAN_PS;\n}\n\n/** The account's drafts, newest first. Bodies are previewed, never returned whole. */\nexport async function listOutlookDrafts(request: ListDraftsRequest): Promise<ListDraftsResult> {\n const output = await runPowerShellJson(`${draftsScript(request.account)}\n$previewChars = ${psInt(request.previewChars)}\n$limit = ${psInt(request.limit)}\n$rows = @()\n$folderPaths = @()\nforeach ($entry in $scan) {\n $folderPath = ''\n try { $folderPath = [string]$entry.folder.FolderPath } catch {}\n $folderPaths += $folderPath\n foreach ($it in $entry.folder.Items) {\n if (-not (Test-DraftMatches $it $entry.includeUnstamped)) { continue }\n $addresses = @()\n try {\n foreach ($r in $it.Recipients) {\n $address = ''\n try { $address = [string]$r.Address } catch {}\n # An unresolved Exchange entry has a DN for an address; its name reads better.\n if ($address.StartsWith('/')) { try { $address = [string]$r.Name } catch {} }\n if ($address) { $addresses += $address }\n }\n } catch {}\n $preview = ''\n if ($previewChars -gt 0) {\n try { $preview = ([string]$it.Body -replace '\\\\s+', ' ').Trim() } catch {}\n if ($preview.Length -gt $previewChars) { $preview = $preview.Substring(0, $previewChars) }\n }\n $modified = ''\n try { $modified = $it.LastModificationTime.ToString('yyyy-MM-dd HH:mm') } catch {}\n $hasAttachments = $false\n try { $hasAttachments = ($it.Attachments.Count -gt 0) } catch {}\n $rows += [PSCustomObject]@{\n entryId = [string]$it.EntryID\n subject = [string]$it.Subject\n to = [string]$it.To\n toEmails = @($addresses)\n bodyPreview = $preview\n hasAttachments = $hasAttachments\n lastModified = $modified\n folderPath = $folderPath\n }\n }\n}\n$sorted = @($rows | Sort-Object -Property lastModified -Descending)\n$total = $sorted.Count\nif ($total -gt $limit) { $sorted = @($sorted[0..($limit - 1)]) }\nConvertTo-Json -Depth 4 -InputObject ([PSCustomObject]@{\n account = $target\n foldersScanned = @($folderPaths)\n count = $total\n truncated = ($total -gt $limit)\n drafts = @($sorted)\n})\n`, 'scan');\n const e = record(output);\n return {\n account: str(e.account) || request.account,\n foldersScanned: toArray(e.foldersScanned).map(str),\n count: num(e.count),\n truncated: bool(e.truncated),\n drafts: toArray(e.drafts).map(row => {\n const d = record(row);\n return {\n entryId: str(d.entryId),\n subject: str(d.subject),\n to: str(d.to),\n toEmails: strList(d.toEmails),\n bodyPreview: str(d.bodyPreview),\n hasAttachments: bool(d.hasAttachments),\n lastModified: str(d.lastModified),\n folderPath: str(d.folderPath),\n };\n }),\n };\n}\n\n/** Run `verb` over the named drafts of the account, each proved to be its own first. */\nasync function actOnDrafts(\n request: EntryIdsRequest,\n verb: 'send' | 'delete',\n): Promise<{ done: number; failed: ItemFailure[] }> {\n const output = await runPowerShellJson(`${draftsScript(request.account)}\n$done = 0\n$failed = @()\nforeach ($id in ${psArray(request.entryIds)}) {\n $subject = ''\n try {\n $draft = Resolve-OwnDraft $id '${verb}'\n try { $subject = [string]$draft.Subject } catch {}\n $draft.${verb === 'send' ? 'Send' : 'Delete'}()\n $done++\n } catch {\n ${itemFailurePs('$id')}\n }\n}\nConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{ done = $done; failed = @($failed) })\n`, 'scan');\n const e = record(output);\n return { done: num(e.done), failed: itemFailures(e.failed) };\n}\n\n/** Send the named drafts. */\nexport async function sendDrafts(request: EntryIdsRequest): Promise<SendDraftsResult> {\n const { done, failed } = await actOnDrafts(request, 'send');\n return { sent: done, failed };\n}\n\n/**\n * Send every draft belonging to the account.\n *\n * The matching items are snapshotted before the first send: sending moves an\n * item out of Drafts, and sending during a live enumeration would skip every\n * other one.\n */\nexport async function sendAllDrafts(request: AccountRequest): Promise<SendDraftsResult> {\n const output = await runPowerShellJson(`${draftsScript(request.account)}\n$drafts = @()\nforeach ($entry in $scan) {\n foreach ($it in $entry.folder.Items) {\n if (Test-DraftMatches $it $entry.includeUnstamped) { $drafts += $it }\n }\n}\n$sent = 0\n$failed = @()\nforeach ($draft in $drafts) {\n $entryId = ''\n $subject = ''\n try { $entryId = [string]$draft.EntryID } catch {}\n try { $subject = [string]$draft.Subject } catch {}\n try {\n $draft.Send()\n $sent++\n } catch {\n ${itemFailurePs('$entryId')}\n }\n}\nConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{ sent = $sent; failed = @($failed) })\n`, 'scan');\n const e = record(output);\n return { sent: num(e.sent), failed: itemFailures(e.failed) };\n}\n\n/**\n * Delete the named drafts. Outlook's Delete() moves an item to Deleted Items,\n * so a mistake stays recoverable. Without the proof that each is this\n * account's draft, this would be a general \"delete any email by id\" call,\n * which is not what it is for.\n */\nexport async function deleteOutlookDrafts(request: EntryIdsRequest): Promise<DeleteDraftsResult> {\n const { done, failed } = await actOnDrafts(request, 'delete');\n return { deleted: done, failed };\n}\n"]}
@@ -60,7 +60,7 @@ foreach ($id in ${(0, run_1.psArray)(request.entryIds)}) {
60
60
  [void]$it.Move($scope)
61
61
  $moved++
62
62
  } catch {
63
- $failed += [PSCustomObject]@{ entryId = $id; subject = $subject; error = $_.Exception.Message }
63
+ ${(0, json_1.itemFailurePs)('$id')}
64
64
  }
65
65
  }
66
66
  ConvertTo-Json -Depth 3 -InputObject ([PSCustomObject]@{
@@ -1 +1 @@
1
- {"version":3,"file":"folders.js","sourceRoot":"","sources":["../../src/windows/folders.ts"],"names":[],"mappings":";;AASA,4CA+BC;AAQD,8CA+BC;AA/ED,gEAAgE;AAChE,+BAA0D;AAC1D,uCAA8E;AAC9E,yCAA+E;AAC/E,kCAAmC;AAInC,yDAAyD;AAClD,KAAK,UAAU,gBAAgB,CAAC,OAA2B;IAC9D,MAAM,MAAM,GAAG,MAAM,IAAA,uBAAiB,EAAC,GAAG,IAAA,uBAAa,EAAC,OAAO,CAAC,OAAO,CAAC;EAC1E,2BAAiB;mCACgB,IAAA,WAAK,EAAC,eAAQ,CAAC,KAAK,CAAC;cAC1C,IAAA,WAAK,EAAC,OAAO,CAAC,QAAQ,CAAC;;;;;;;;;;;;;;;;;CAiBpC,EAAE,UAAU,CAAC,CAAC;IACX,OAAO,IAAA,cAAO,EAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;QAC7B,MAAM,CAAC,GAAG,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC;QACtB,OAAO;YACH,IAAI,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,IAAI,CAAC;YACjB,UAAU,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,UAAU,CAAC;YAC7B,SAAS,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,SAAS,CAAC;YAC3B,KAAK,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;SAC3B,CAAC;IACN,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,iBAAiB,CAAC,OAAoB;IACxD,MAAM,MAAM,GAAG,MAAM,IAAA,uBAAiB,EAAC,GAAG,IAAA,uBAAa,EAAC,OAAO,CAAC,OAAO,CAAC;EAC1E,2BAAiB;EACjB,IAAA,yBAAe,EAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,eAAe,CAAC;;;kBAG7D,IAAA,aAAO,EAAC,OAAO,CAAC,QAAQ,CAAC;;;;;;;;;;;;;;;;;CAiB1C,EAAE,MAAM,CAAC,CAAC;IACP,MAAM,CAAC,GAAG,IAAA,aAAM,EAAC,MAAM,CAAC,CAAC;IACzB,OAAO;QACH,UAAU,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,UAAU,CAAC;QAC7B,aAAa,EAAE,IAAA,WAAI,EAAC,CAAC,CAAC,aAAa,CAAC;QACpC,KAAK,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,KAAK,CAAC;QACnB,MAAM,EAAE,IAAA,mBAAY,EAAC,CAAC,CAAC,MAAM,CAAC;KACjC,CAAC;AACN,CAAC","sourcesContent":["// Listing the folders under an Inbox, and filing mail into one.\nimport { psArray, psInt, runPowerShellJson } from './run';\nimport { accountScript, DELIVERY_STORE_PS, mailScopeScript } from './scripts';\nimport { bool, itemFailures, num, record, str, toArray } from '../shared/json';\nimport { FolderId } from '../mail';\nimport type { ListFoldersRequest, MoveRequest } from '../backend';\nimport type { InboxFolderInfo, MoveEmailsResult } from '../types';\n\n/** The folders under an account's Inbox, depth-first. */\nexport async function listInboxFolders(request: ListFoldersRequest): Promise<InboxFolderInfo[]> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n$inbox = $store.GetDefaultFolder(${psInt(FolderId.Inbox)})\n$maxDepth = ${psInt(request.maxDepth)}\n$rows = New-Object System.Collections.ArrayList\nfunction Add-Folders($folder, [int]$level) {\n foreach ($f in $folder.Folders) {\n $itemCount = 0\n try { $itemCount = [int]$f.Items.Count } catch {}\n [void]$rows.Add([PSCustomObject]@{\n name = [string]$f.Name\n folderPath = [string]$f.FolderPath\n itemCount = $itemCount\n depth = $level\n })\n if ($level -lt $maxDepth) { Add-Folders $f ($level + 1) }\n }\n}\nAdd-Folders $inbox 1\nConvertTo-Json -Depth 3 -InputObject @($rows)\n`, 'standard');\n return toArray(output).map(row => {\n const e = record(row);\n return {\n name: str(e.name),\n folderPath: str(e.folderPath),\n itemCount: num(e.itemCount),\n depth: num(e.depth) || 1,\n };\n });\n}\n\n/**\n * File emails into a folder of the account: one under the Inbox, or a\n * well-known one by name. Moving to Deleted Items is how mail is deleted\n * recoverably. Moving rewrites each item's entry id, so the ids passed in are\n * dead afterwards.\n */\nexport async function moveOutlookEmails(request: MoveRequest): Promise<MoveEmailsResult> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n${mailScopeScript(request.folder, request.folderLabel, request.createIfMissing)}\n$moved = 0\n$failed = @()\nforeach ($id in ${psArray(request.entryIds)}) {\n $subject = ''\n try {\n $it = $ns.GetItemFromID($id, $store.StoreID)\n try { $subject = [string]$it.Subject } catch {}\n [void]$it.Move($scope)\n $moved++\n } catch {\n $failed += [PSCustomObject]@{ entryId = $id; subject = $subject; error = $_.Exception.Message }\n }\n}\nConvertTo-Json -Depth 3 -InputObject ([PSCustomObject]@{\n folderPath = [string]$scope.FolderPath\n folderCreated = $scopeCreated\n moved = $moved\n failed = @($failed)\n})\n`, 'scan');\n const e = record(output);\n return {\n folderPath: str(e.folderPath),\n folderCreated: bool(e.folderCreated),\n moved: num(e.moved),\n failed: itemFailures(e.failed),\n };\n}\n"]}
1
+ {"version":3,"file":"folders.js","sourceRoot":"","sources":["../../src/windows/folders.ts"],"names":[],"mappings":";;AASA,4CA+BC;AAQD,8CA+BC;AA/ED,gEAAgE;AAChE,+BAA0D;AAC1D,uCAA8E;AAC9E,yCAA8F;AAC9F,kCAAmC;AAInC,yDAAyD;AAClD,KAAK,UAAU,gBAAgB,CAAC,OAA2B;IAC9D,MAAM,MAAM,GAAG,MAAM,IAAA,uBAAiB,EAAC,GAAG,IAAA,uBAAa,EAAC,OAAO,CAAC,OAAO,CAAC;EAC1E,2BAAiB;mCACgB,IAAA,WAAK,EAAC,eAAQ,CAAC,KAAK,CAAC;cAC1C,IAAA,WAAK,EAAC,OAAO,CAAC,QAAQ,CAAC;;;;;;;;;;;;;;;;;CAiBpC,EAAE,UAAU,CAAC,CAAC;IACX,OAAO,IAAA,cAAO,EAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;QAC7B,MAAM,CAAC,GAAG,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC;QACtB,OAAO;YACH,IAAI,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,IAAI,CAAC;YACjB,UAAU,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,UAAU,CAAC;YAC7B,SAAS,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,SAAS,CAAC;YAC3B,KAAK,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;SAC3B,CAAC;IACN,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,iBAAiB,CAAC,OAAoB;IACxD,MAAM,MAAM,GAAG,MAAM,IAAA,uBAAiB,EAAC,GAAG,IAAA,uBAAa,EAAC,OAAO,CAAC,OAAO,CAAC;EAC1E,2BAAiB;EACjB,IAAA,yBAAe,EAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,eAAe,CAAC;;;kBAG7D,IAAA,aAAO,EAAC,OAAO,CAAC,QAAQ,CAAC;;;;;;;;UAQjC,IAAA,oBAAa,EAAC,KAAK,CAAC;;;;;;;;;CAS7B,EAAE,MAAM,CAAC,CAAC;IACP,MAAM,CAAC,GAAG,IAAA,aAAM,EAAC,MAAM,CAAC,CAAC;IACzB,OAAO;QACH,UAAU,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,UAAU,CAAC;QAC7B,aAAa,EAAE,IAAA,WAAI,EAAC,CAAC,CAAC,aAAa,CAAC;QACpC,KAAK,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,KAAK,CAAC;QACnB,MAAM,EAAE,IAAA,mBAAY,EAAC,CAAC,CAAC,MAAM,CAAC;KACjC,CAAC;AACN,CAAC","sourcesContent":["// Listing the folders under an Inbox, and filing mail into one.\nimport { psArray, psInt, runPowerShellJson } from './run';\nimport { accountScript, DELIVERY_STORE_PS, mailScopeScript } from './scripts';\nimport { bool, itemFailurePs, itemFailures, num, record, str, toArray } from '../shared/json';\nimport { FolderId } from '../mail';\nimport type { ListFoldersRequest, MoveRequest } from '../backend';\nimport type { InboxFolderInfo, MoveEmailsResult } from '../types';\n\n/** The folders under an account's Inbox, depth-first. */\nexport async function listInboxFolders(request: ListFoldersRequest): Promise<InboxFolderInfo[]> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n$inbox = $store.GetDefaultFolder(${psInt(FolderId.Inbox)})\n$maxDepth = ${psInt(request.maxDepth)}\n$rows = New-Object System.Collections.ArrayList\nfunction Add-Folders($folder, [int]$level) {\n foreach ($f in $folder.Folders) {\n $itemCount = 0\n try { $itemCount = [int]$f.Items.Count } catch {}\n [void]$rows.Add([PSCustomObject]@{\n name = [string]$f.Name\n folderPath = [string]$f.FolderPath\n itemCount = $itemCount\n depth = $level\n })\n if ($level -lt $maxDepth) { Add-Folders $f ($level + 1) }\n }\n}\nAdd-Folders $inbox 1\nConvertTo-Json -Depth 3 -InputObject @($rows)\n`, 'standard');\n return toArray(output).map(row => {\n const e = record(row);\n return {\n name: str(e.name),\n folderPath: str(e.folderPath),\n itemCount: num(e.itemCount),\n depth: num(e.depth) || 1,\n };\n });\n}\n\n/**\n * File emails into a folder of the account: one under the Inbox, or a\n * well-known one by name. Moving to Deleted Items is how mail is deleted\n * recoverably. Moving rewrites each item's entry id, so the ids passed in are\n * dead afterwards.\n */\nexport async function moveOutlookEmails(request: MoveRequest): Promise<MoveEmailsResult> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n${mailScopeScript(request.folder, request.folderLabel, request.createIfMissing)}\n$moved = 0\n$failed = @()\nforeach ($id in ${psArray(request.entryIds)}) {\n $subject = ''\n try {\n $it = $ns.GetItemFromID($id, $store.StoreID)\n try { $subject = [string]$it.Subject } catch {}\n [void]$it.Move($scope)\n $moved++\n } catch {\n ${itemFailurePs('$id')}\n }\n}\nConvertTo-Json -Depth 3 -InputObject ([PSCustomObject]@{\n folderPath = [string]$scope.FolderPath\n folderCreated = $scopeCreated\n moved = $moved\n failed = @($failed)\n})\n`, 'scan');\n const e = record(output);\n return {\n folderPath: str(e.folderPath),\n folderCreated: bool(e.folderCreated),\n moved: num(e.moved),\n failed: itemFailures(e.failed),\n };\n}\n"]}
@@ -37,7 +37,7 @@ ${scripts_1.NAMED_STORE_PS}
37
37
  ${(0, scripts_1.mailScopeScript)(request.folder, request.folderLabel)}
38
38
  $scopePath = [string]$scope.FolderPath
39
39
  ${(0, scripts_1.cutoffScript)(request.daysBack)}
40
- ${(0, scripts_1.itemsSinceScript)('scope', dateProp, 'filtered')}
40
+ ${(0, scripts_1.itemsSinceScript)('scope', dateProp)}
41
41
  $filtered.Sort('[${dateProp}]', $true)
42
42
  $limit = ${(0, run_1.psInt)(request.limit)}
43
43
  $previewChars = ${(0, run_1.psInt)(request.previewChars)}
@@ -226,7 +226,7 @@ $seen = @{}
226
226
  for ($fx = 0; $fx -lt $folders.Count; $fx++) {
227
227
  if (-not $inScope[$fx]) { continue }
228
228
  $folder = $folders[$fx]
229
- ${(0, scripts_1.itemsSinceScript)('folder', 'ReceivedTime', 'filtered').trim()}
229
+ ${(0, scripts_1.itemsSinceScript)('folder', 'ReceivedTime').trim()}
230
230
  if ($subjectDasl) {
231
231
  try {
232
232
  $candidate = $filtered.Restrict($subjectQuery)
@@ -317,6 +317,8 @@ $bodyRaw = ''
317
317
  try { $bodyRaw = [string]$item.Body } catch {}
318
318
  $storeId = ''
319
319
  try { $storeId = [string]$item.Parent.Store.StoreID } catch {}
320
+ $folderPath = ''
321
+ try { $folderPath = [string]$item.Parent.FolderPath } catch {}
320
322
  ${scripts_1.SENDER_SMTP_PS}
321
323
  $received = ''
322
324
  try { $received = $item.ReceivedTime.ToString('yyyy-MM-dd HH:mm') } catch {}
@@ -329,6 +331,7 @@ ConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{
329
331
  receivedTime = $received
330
332
  body = ${BODY_B64_PS}
331
333
  attachmentNames = @($attachmentNames)
334
+ folderPath = $folderPath
332
335
  })
333
336
  `, 'quick');
334
337
  const e = (0, json_1.record)(output);
@@ -341,6 +344,7 @@ ConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{
341
344
  receivedTime: (0, json_1.str)(e.receivedTime),
342
345
  body: (0, json_1.base64Text)(e.body),
343
346
  attachmentNames: (0, json_1.strList)(e.attachmentNames),
347
+ folderPath: (0, json_1.str)(e.folderPath),
344
348
  };
345
349
  }
346
350
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/windows/read.ts"],"names":[],"mappings":";;AAqCA,0CAsEC;AASD,sCAoCC;AAYD,kDAuIC;AAGD,8CAmDC;AASD,4CAKC;AA/WD,yEAAyE;AACzE,mCAAmC;AACnC,+BAA2F;AAC3F,uCASmB;AACnB,yCAA2E;AAC3E,kCAA+G;AAC/G,sCAAuC;AAIvC;;;;GAIG;AACH,SAAS,YAAY,CAAC,MAAc;IAChC,IAAI,MAAM,KAAK,eAAQ,CAAC,MAAM;QAAE,OAAO,sBAAsB,CAAC;IAC9D,OAAO,IAAA,qBAAc,EAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC;AAC9D,CAAC;AAED;;;;GAIG;AACH,MAAM,WAAW,GAAG,oFAAoF,CAAC;AAEzG,mEAAmE;AAC5D,KAAK,UAAU,eAAe,CAAC,OAAyB;IAC3D,MAAM,QAAQ,GAAG,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACrD,MAAM,QAAQ,GAAG,IAAA,qBAAc,EAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACvD,MAAM,MAAM,GAAG,MAAM,IAAA,uBAAiB,EAAC,GAAG,IAAA,uBAAa,EAAC,OAAO,CAAC,OAAO,CAAC;EAC1E,wBAAc;EACd,IAAA,yBAAe,EAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC;;EAEpD,IAAA,sBAAY,EAAC,OAAO,CAAC,QAAQ,CAAC;EAC9B,IAAA,0BAAgB,EAAC,OAAO,EAAE,QAAQ,EAAE,UAAU,CAAC;mBAC9B,QAAQ;WAChB,IAAA,WAAK,EAAC,OAAO,CAAC,KAAK,CAAC;kBACb,IAAA,WAAK,EAAC,OAAO,CAAC,YAAY,CAAC;;;;;;;;2BAQlB,QAAQ;;;;;;;;;;;;;UAazB,IAAA,YAAM,EAAC,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;CAqBzB,EAAE,UAAU,CAAC,CAAC;IACX,OAAO,IAAA,cAAO,EAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;QAC7B,MAAM,CAAC,GAAG,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC;QACtB,MAAM,eAAe,GAAG,IAAA,cAAO,EAAC,CAAC,CAAC,eAAe,CAAC,CAAC,GAAG,CAAC,UAAG,CAAC,CAAC;QAC5D,OAAO;YACH,OAAO,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,OAAO,CAAC;YACvB,OAAO,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,OAAO,CAAC;YACvB,OAAO,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,OAAO,CAAC;YACvB,UAAU,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,UAAU,CAAC;YAC7B,WAAW,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,WAAW,CAAC;YAC/B,YAAY,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,YAAY,CAAC;YACjC,WAAW,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,WAAW,CAAC;YAC/B,eAAe;YACf,eAAe,EAAE,eAAe,CAAC,MAAM;YACvC,UAAU,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,UAAU,CAAC;SAChC,CAAC;IACN,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,aAAa,CAAC,KAAmB;IACnD,MAAM,MAAM,GAAG,MAAM,IAAA,uBAAiB,EAAC,GAAG,oBAAU;EACtD,IAAA,0BAAgB,EAAC,KAAK,CAAC;;;;;aAKZ,IAAA,mBAAU,EAAC,WAAW,EAAE,OAAO,CAAC;;;;;;EAM3C,wBAAc;;;;;;;;;wBASQ,WAAW;;;CAGlC,EAAE,OAAO,CAAC,CAAC;IACR,MAAM,CAAC,GAAG,IAAA,aAAM,EAAC,MAAM,CAAC,CAAC;IACzB,OAAO;QACH,OAAO,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,OAAO;QACxC,OAAO,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,OAAO,CAAC;QACvB,UAAU,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,UAAU,CAAC;QAC7B,WAAW,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,WAAW,CAAC;QAC/B,YAAY,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,YAAY,CAAC;QACjC,IAAI,EAAE,IAAA,iBAAU,EAAC,CAAC,CAAC,IAAI,CAAC;QACxB,eAAe,EAAE,IAAA,cAAO,EAAC,CAAC,CAAC,eAAe,CAAC,CAAC,GAAG,CAAC,UAAG,CAAC;KACvD,CAAC;AACN,CAAC;AAED;;;;;;;;;GASG;AACI,KAAK,UAAU,mBAAmB,CAAC,OAAsB;IAC5D,MAAM,eAAe,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,IAAI,OAAO,CAAC,cAAc,IAAI,OAAO,CAAC,cAAc,CAAC,CAAC;IACpG,6EAA6E;IAC7E,0EAA0E;IAC1E,kEAAkE;IAClE,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3G,MAAM,MAAM,GAAG,MAAM,IAAA,uBAAiB,EAAC,GAAG,IAAA,uBAAa,EAAC,OAAO,CAAC,OAAO,CAAC;EAC1E,wBAAc;mCACmB,IAAA,WAAK,EAAC,eAAQ,CAAC,KAAK,CAAC;iBACvC,IAAA,cAAQ,EAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,IAAA,wBAAiB,EAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;iBAC3E,IAAA,cAAQ,EAAC,WAAW,CAAC;oBAClB,IAAA,cAAQ,EAAC,OAAO,CAAC,cAAc,EAAE,MAAM,IAAI,EAAE,CAAC;kBAChD,IAAA,cAAQ,EAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,0BAAmB,CAAC,CAAC,CAAC,EAAE,CAAC;oBACzD,IAAA,YAAM,EAAC,eAAe,CAAC;uBACpB,IAAA,YAAM,EAAC,OAAO,CAAC,iBAAiB,CAAC;iBACvC,IAAA,YAAM,EAAC,OAAO,CAAC,WAAW,CAAC;EAC1C,IAAA,sBAAY,EAAC,OAAO,CAAC,QAAQ,CAAC;;;;;sBAKV,yBAAkB,CAAC,GAAG,CAAC,WAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;;;;;;eAM/C,IAAA,aAAO,EAAC,OAAO,CAAC,cAAc,CAAC;eAC/B,IAAA,aAAO,EAAC,OAAO,CAAC,cAAc,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MA4CxC,IAAA,0BAAgB,EAAC,QAAQ,EAAE,cAAc,EAAE,UAAU,CAAC,CAAC,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiCjE,wBAAc;;;;;;;;gCAQgB,WAAW;;;;;;;CAO1C,EAAE,MAAM,CAAC,CAAC;IACP,OAAO,IAAA,cAAO,EAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;QAC7B,MAAM,CAAC,GAAG,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC;QACtB,OAAO;YACH,OAAO,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,OAAO,CAAC;YACvB,OAAO,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,OAAO,CAAC;YACvB,OAAO,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,OAAO,CAAC;YACvB,UAAU,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,UAAU,CAAC;YAC7B,WAAW,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,WAAW,CAAC;YAC/B,YAAY,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,YAAY,CAAC;YACjC,IAAI,EAAE,IAAA,iBAAU,EAAC,CAAC,CAAC,IAAI,CAAC;YACxB,eAAe,EAAE,IAAA,cAAO,EAAC,CAAC,CAAC,eAAe,CAAC,CAAC,GAAG,CAAC,UAAG,CAAC;YACpD,UAAU,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,UAAU,CAAC;SAChC,CAAC;IACN,CAAC,CAAC,CAAC;AACP,CAAC;AAED,2DAA2D;AACpD,KAAK,UAAU,iBAAiB;IACnC,MAAM,MAAM,GAAG,MAAM,IAAA,uBAAiB,EAAC;;;;;;;;;;;;;;aAc9B,IAAA,mBAAU,EAAC,WAAW,EAAE,OAAO,CAAC;;;aAGhC,IAAA,mBAAU,EAAC,iBAAiB,CAAC;;;;;;;;EAQxC,wBAAc;;;;;;;;;;wBAUQ,WAAW;;;CAGlC,EAAE,OAAO,CAAC,CAAC;IACR,MAAM,CAAC,GAAG,IAAA,aAAM,EAAC,MAAM,CAAC,CAAC;IACzB,OAAO;QACH,OAAO,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,OAAO,CAAC;QACvB,OAAO,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,OAAO,CAAC;QACvB,OAAO,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,OAAO,CAAC;QACvB,UAAU,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,UAAU,CAAC;QAC7B,WAAW,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,WAAW,CAAC;QAC/B,YAAY,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,YAAY,CAAC;QACjC,IAAI,EAAE,IAAA,iBAAU,EAAC,CAAC,CAAC,IAAI,CAAC;QACxB,eAAe,EAAE,IAAA,cAAO,EAAC,CAAC,CAAC,eAAe,CAAC;KAC9C,CAAC;AACN,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,gBAAgB,CAAC,KAAmB;IACtD,MAAM,IAAA,mBAAa,EAAC,GAAG,oBAAU;EACnC,IAAA,0BAAgB,EAAC,KAAK,CAAC;;CAExB,EAAE,OAAO,CAAC,CAAC;AACZ,CAAC","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} from './scripts';\nimport { base64Text, record, str, strList, toArray } from '../shared/json';\nimport { FolderId, isOutgoingRoot, NON_INCOMING_ROOTS, REPLY_PREFIX_SOURCE, subjectGlobSource } from '../mail';\nimport { failureTag } from '../errors';\nimport type { RawEmail, ReadInboxRequest, SearchRequest } from '../backend';\nimport type { EmailLocator, InboxEmail, InboxSearchMatch, SelectedEmail } from '../types';\n\n/**\n * The COM property carrying each kind of folder's timestamp. Sent Items and\n * Drafts have no ReceivedTime, and filtering them on it returns an empty set\n * that looks exactly like an empty folder.\n */\nfunction dateProperty(rootId: number): string {\n if (rootId === FolderId.Drafts) return 'LastModificationTime';\n return isOutgoingRoot(rootId) ? 'SentOn' : 'ReceivedTime';\n}\n\n/**\n * Bodies cross the PowerShell boundary base64-encoded: they carry quotes,\n * control characters and non-ASCII that would otherwise have to survive JSON\n * escaping in two languages intact.\n */\nconst BODY_B64_PS = `[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes([string]$bodyRaw))`;\n\n/** Recent mail from the Inbox root or one folder, newest first. */\nexport async function readInboxEmails(request: ReadInboxRequest): Promise<InboxEmail[]> {\n const dateProp = dateProperty(request.folder.rootId);\n const outgoing = isOutgoingRoot(request.folder.rootId);\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${NAMED_STORE_PS}\n${mailScopeScript(request.folder, request.folderLabel)}\n$scopePath = [string]$scope.FolderPath\n${cutoffScript(request.daysBack)}\n${itemsSinceScript('scope', dateProp)}\n$filtered.Sort('[${dateProp}]', $true)\n$limit = ${psInt(request.limit)}\n$previewChars = ${psInt(request.previewChars)}\n$rows = @()\n$count = $filtered.Count\nfor ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {\n $item = $null\n try { $item = $filtered.Item($i) } catch {}\n if ($item -eq $null) { continue }\n $stamp = $null\n try { $stamp = $item.${dateProp} } catch {}\n # Sorted newest first, so the first item older than the window ends it.\n if ($stamp -ne $null -and $stamp -lt $cutoff) { break }\n $preview = ''\n if ($previewChars -gt 0) {\n $bodyText = ''\n try { $bodyText = [string]$item.Body } catch {}\n $preview = if ($bodyText.Length -gt $previewChars) { $bodyText.Substring(0, $previewChars) } else { $bodyText }\n }\n $attachmentNames = @()\n try { foreach ($att in $item.Attachments) { $attachmentNames += [string]$att.FileName } } catch {}\n $who = ''\n $whoEmail = ''\n if (${psBool(outgoing)}) {\n # Outgoing mail reports who it went TO; \"from\" is always the mailbox itself.\n try { $who = [string]$item.To } catch {}\n $whoEmail = $who\n } else {\n try { $who = [string]$item.SenderName } catch {}\n try { $whoEmail = [string]$item.SenderEmailAddress } catch {}\n }\n $rows += [PSCustomObject]@{\n entryId = [string]$item.EntryID\n storeId = $storeId\n folderPath = $scopePath\n subject = ([string]$item.Subject).Trim()\n senderName = $who\n senderEmail = $whoEmail\n receivedTime = if ($stamp -ne $null) { $stamp.ToString('yyyy-MM-dd HH:mm') } else { '' }\n bodyPreview = $preview\n attachmentNames = @($attachmentNames)\n }\n}\nConvertTo-Json -Depth 4 -InputObject @($rows)\n`, 'standard');\n return toArray(output).map(row => {\n const e = record(row);\n const attachmentNames = toArray(e.attachmentNames).map(str);\n return {\n entryId: str(e.entryId),\n storeId: str(e.storeId),\n subject: str(e.subject),\n senderName: str(e.senderName),\n senderEmail: str(e.senderEmail),\n receivedTime: str(e.receivedTime),\n bodyPreview: str(e.bodyPreview),\n attachmentNames,\n attachmentCount: attachmentNames.length,\n folderPath: str(e.folderPath),\n };\n });\n}\n\n/**\n * One email's full plain-text body, by id.\n *\n * `Body`, not `HTMLBody`: Outlook's own plain-text rendering is what every other\n * reader here consumes, and markup costs an order of magnitude more for the same\n * sentences. The tradeoff is that an HTML table's rows flatten.\n */\nexport async function readEmailBody(email: EmailLocator): Promise<RawEmail> {\n const output = await runPowerShellJson(`${SESSION_PS}\n${itemLookupScript(email)}\n$class = 0\ntry { $class = [int]$item.Class } catch {}\n# 43 = olMail, 46 = olReport (a non-delivery report is still worth reading).\nif ($class -ne 43 -and $class -ne 46) {\n throw \"${failureTag('NOT_FOUND', 'email')}The item with entry id '$lookupId' is not an email (class $class).\"\n}\n$bodyRaw = ''\ntry { $bodyRaw = [string]$item.Body } catch {}\n$attachmentNames = @()\ntry { foreach ($att in $item.Attachments) { $attachmentNames += [string]$att.FileName } } catch {}\n${SENDER_SMTP_PS}\n$received = ''\ntry { $received = $item.ReceivedTime.ToString('yyyy-MM-dd HH:mm') } catch {}\nConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{\n entryId = [string]$item.EntryID\n subject = ([string]$item.Subject).Trim()\n senderName = [string]$item.SenderName\n senderEmail = $senderSmtp\n receivedTime = $received\n body = ${BODY_B64_PS}\n attachmentNames = @($attachmentNames)\n})\n`, 'quick');\n const e = record(output);\n return {\n entryId: str(e.entryId) || email.entryId,\n subject: str(e.subject),\n senderName: str(e.senderName),\n senderEmail: str(e.senderEmail),\n receivedTime: str(e.receivedTime),\n body: base64Text(e.body),\n attachmentNames: toArray(e.attachmentNames).map(str),\n };\n}\n\n/**\n * Walk every folder under the Inbox for the mail matching the request.\n *\n * The window is enforced twice — a date Restrict per folder, then a per-item\n * check — because Restrict is not reliable across stores, and a scan that\n * silently widens on some mailboxes is the failure this guards against.\n * `subjectLike` also drives a server-side prefilter, purely as an optimization:\n * every subject is re-checked, so a store that can't run the query (IMAP stores\n * accept it and match nothing) returns the same set, only slower.\n */\nexport async function searchInboxByFilter(request: SearchRequest): Promise<InboxSearchMatch[]> {\n const subjectFiltered = !!(request.subjectLike || request.subjectPattern || request.excludeReplies);\n // The server-side prefilter speaks DASL, whose `like` wildcards are % and _.\n // A literal % or _ in the glob is left as a wildcard there: that can only\n // widen the prefilter, and the exact test below narrows it again.\n const subjectDasl = request.subjectLike ? request.subjectLike.replace(/\\*/g, '%').replace(/\\?/g, '_') : '';\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${NAMED_STORE_PS}\n$inbox = $store.GetDefaultFolder(${psInt(FolderId.Inbox)})\n$subjectLike = ${psString(request.subjectLike ? subjectGlobSource(request.subjectLike) : '')}\n$subjectDasl = ${psString(subjectDasl)}\n$subjectPattern = ${psString(request.subjectPattern?.source ?? '')}\n$replyPattern = ${psString(request.excludeReplies ? REPLY_PREFIX_SOURCE : '')}\n$requireSubject = ${psBool(subjectFiltered)}\n$requireAttachment = ${psBool(request.requireAttachment)}\n$includeBody = ${psBool(request.includeBody)}\n${cutoffScript(request.daysBack)}\n# Sent, Drafts, Deleted and Junk are siblings of the Inbox on an Exchange profile\n# and CHILDREN of it on an IMAP one. They are cut by id — their names are\n# localized — before the walk, so each takes its whole subtree with it.\n$skipIds = @{}\nforeach ($role in @(${NON_INCOMING_ROOTS.map(psInt).join(', ')})) {\n try {\n $roleFolder = $store.GetDefaultFolder($role)\n if ($roleFolder -ne $null) { $skipIds[[string]$roleFolder.EntryID] = $true }\n } catch {}\n}\n$skipNames = ${psArray(request.excludeFolders)}\n$onlyNames = ${psArray(request.includeFolders)}\nfunction Test-FolderNamed([string]$name, [string]$path, $list) {\n foreach ($entry in $list) {\n if ($name -ieq $entry -or $path -ieq $entry) { return $true }\n }\n return $false\n}\n# Traversal and scanning are separate, and the two lists are not symmetric: an\n# excluded folder is never queued, so its subtree goes with it, while an\n# included one only marks a folder the walk reached anyway — the walk still\n# passes through folders nobody named to reach a nested one somebody did.\n$rootName = ''\ntry { $rootName = [string]$inbox.Name } catch {}\n$rootPath = ''\ntry { $rootPath = [string]$inbox.FolderPath } catch {}\n$folders = [System.Collections.ArrayList]@($inbox)\n$inScope = [System.Collections.ArrayList]@(($onlyNames.Count -eq 0) -or (Test-FolderNamed $rootName $rootPath $onlyNames))\n$walked = 0\nwhile ($walked -lt $folders.Count) {\n try {\n foreach ($sub in $folders[$walked].Folders) {\n if ($skipIds.ContainsKey([string]$sub.EntryID)) { continue }\n $subName = ''\n try { $subName = [string]$sub.Name } catch {}\n $subPath = ''\n try { $subPath = [string]$sub.FolderPath } catch {}\n if (Test-FolderNamed $subName $subPath $skipNames) { continue }\n [void]$folders.Add($sub)\n [void]$inScope.Add(($onlyNames.Count -eq 0) -or (Test-FolderNamed $subName $subPath $onlyNames))\n }\n } catch {}\n $walked++\n}\n# The subject prefilter is DASL rather than Jet: some stores reject Jet's\n# \"[Subject] like\" outright, while DASL is accepted wherever Restrict is. The\n# two syntaxes can't share one filter, so it is a second Restrict on top of the\n# date one. The pattern is concatenated into the query, never interpolated.\n$subjectQuery = '@SQL=' + [char]34 + 'urn:schemas:httpmail:subject' + [char]34 + ' like ' + [char]39 + $subjectDasl.Replace(\"'\", \"''\") + [char]39\n# A list, not an array: += copies the whole array per row, and a wide scan returns thousands.\n$rows = New-Object System.Collections.ArrayList\n$seen = @{}\nfor ($fx = 0; $fx -lt $folders.Count; $fx++) {\n if (-not $inScope[$fx]) { continue }\n $folder = $folders[$fx]\n ${itemsSinceScript('folder', 'ReceivedTime').trim()}\n if ($subjectDasl) {\n try {\n $candidate = $filtered.Restrict($subjectQuery)\n if ($candidate.Count -gt 0) { $filtered = $candidate }\n } catch {}\n }\n $folderPath = ''\n try { $folderPath = [string]$folder.FolderPath } catch {}\n $count = $filtered.Count\n for ($i = 1; $i -le $count; $i++) {\n $item = $null\n try { $item = $filtered.Item($i) } catch {}\n # A restricted collection can hand back nothing for an index, and an item\n # with no id can't be acted on by anything this package does.\n if ($item -eq $null) { continue }\n $entryId = [string]$item.EntryID\n if (-not $entryId -or $seen.ContainsKey($entryId)) { continue }\n $seen[$entryId] = $true\n $subject = ''\n try { $subject = ([string]$item.Subject).Trim() } catch {}\n if ($requireSubject -and -not $subject) { continue }\n if ($subjectLike -and $subject -notmatch $subjectLike) { continue }\n if ($subjectPattern -and $subject -notmatch $subjectPattern) { continue }\n if ($replyPattern -and $subject -imatch $replyPattern) { continue }\n $received = $null\n try { $received = $item.ReceivedTime } catch {}\n if ($received -ne $null -and $received -lt $cutoff) { continue }\n $attachmentNames = @()\n try { foreach ($att in $item.Attachments) { $attachmentNames += [string]$att.FileName } } catch {}\n if ($requireAttachment -and $attachmentNames.Count -eq 0) { continue }\n $bodyRaw = ''\n if ($includeBody) { try { $bodyRaw = [string]$item.Body } catch {} }\n${SENDER_SMTP_PS}\n [void]$rows.Add([PSCustomObject]@{\n entryId = $entryId\n storeId = $storeId\n subject = $subject\n senderName = [string]$item.SenderName\n senderEmail = $senderSmtp\n receivedTime = if ($received -ne $null) { $received.ToString('yyyy-MM-dd HH:mm') } else { '' }\n body = ${BODY_B64_PS}\n attachmentNames = @($attachmentNames)\n folderPath = $folderPath\n })\n }\n}\nConvertTo-Json -Depth 4 -InputObject @($rows)\n`, 'scan');\n return toArray(output).map(row => {\n const e = record(row);\n return {\n entryId: str(e.entryId),\n storeId: str(e.storeId),\n subject: str(e.subject),\n senderName: str(e.senderName),\n senderEmail: str(e.senderEmail),\n receivedTime: str(e.receivedTime),\n body: base64Text(e.body),\n attachmentNames: toArray(e.attachmentNames).map(str),\n folderPath: str(e.folderPath),\n };\n });\n}\n\n/** The email currently selected — or open — in Outlook. */\nexport async function readSelectedEmail(): Promise<SelectedEmail> {\n const output = await runPowerShellJson(`\n$outlook = New-Object -ComObject Outlook.Application\n$item = $null\ntry {\n $explorer = $outlook.ActiveExplorer()\n if ($explorer -ne $null -and $explorer.Selection.Count -ge 1) { $item = $explorer.Selection.Item(1) }\n} catch {}\nif ($item -eq $null) {\n try {\n $inspector = $outlook.ActiveInspector()\n if ($inspector -ne $null) { $item = $inspector.CurrentItem }\n } catch {}\n}\nif ($item -eq $null) {\n throw \"${failureTag('NOT_FOUND', 'email')}No email is selected in Outlook. Select or open an email, then try again.\"\n}\nif ([string]$item.MessageClass -notlike 'IPM.Note*') {\n throw \"${failureTag('INVALID_REQUEST')}The item selected in Outlook is not an email.\"\n}\n$attachmentNames = @()\ntry { foreach ($att in $item.Attachments) { $attachmentNames += [string]$att.FileName } } catch {}\n$bodyRaw = ''\ntry { $bodyRaw = [string]$item.Body } catch {}\n$storeId = ''\ntry { $storeId = [string]$item.Parent.Store.StoreID } catch {}\n$folderPath = ''\ntry { $folderPath = [string]$item.Parent.FolderPath } catch {}\n${SENDER_SMTP_PS}\n$received = ''\ntry { $received = $item.ReceivedTime.ToString('yyyy-MM-dd HH:mm') } catch {}\nConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{\n entryId = [string]$item.EntryID\n storeId = $storeId\n subject = ([string]$item.Subject).Trim()\n senderName = [string]$item.SenderName\n senderEmail = $senderSmtp\n receivedTime = $received\n body = ${BODY_B64_PS}\n attachmentNames = @($attachmentNames)\n folderPath = $folderPath\n})\n`, 'quick');\n const e = record(output);\n return {\n entryId: str(e.entryId),\n storeId: str(e.storeId),\n subject: str(e.subject),\n senderName: str(e.senderName),\n senderEmail: str(e.senderEmail),\n receivedTime: str(e.receivedTime),\n body: base64Text(e.body),\n attachmentNames: strList(e.attachmentNames),\n folderPath: str(e.folderPath),\n };\n}\n\n/**\n * Open an email in Outlook.\n *\n * Entry ids are rewritten when an item MOVES between folders, so an id recorded\n * before its mail was filed can stop resolving. That fails as NOT_FOUND rather\n * than guessing — the caller is the one who knows what the email was.\n */\nexport async function openOutlookEmail(email: EmailLocator): Promise<void> {\n await runPowerShell(`${SESSION_PS}\n${itemLookupScript(email)}\n$item.Display()\n`, 'quick');\n}\n"]}
@@ -1,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. */
@@ -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 generating anything when this isn't Windows. */
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);