@dawnlit/outlook-bridge 1.1.1 → 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 (142) hide show
  1. package/README.md +152 -183
  2. package/dist/backend.d.ts +151 -0
  3. package/dist/backend.js +3 -0
  4. package/dist/backend.js.map +1 -0
  5. package/dist/bridge.d.ts +56 -0
  6. package/dist/bridge.js +507 -0
  7. package/dist/bridge.js.map +1 -0
  8. package/dist/errors.d.ts +79 -43
  9. package/dist/errors.js +93 -52
  10. package/dist/errors.js.map +1 -1
  11. package/dist/index.d.ts +10 -10
  12. package/dist/index.js +25 -52
  13. package/dist/index.js.map +1 -1
  14. package/dist/mac/accounts.d.ts +7 -2
  15. package/dist/mac/accounts.js +23 -6
  16. package/dist/mac/accounts.js.map +1 -1
  17. package/dist/mac/attachments.d.ts +5 -25
  18. package/dist/mac/attachments.js +40 -99
  19. package/dist/mac/attachments.js.map +1 -1
  20. package/dist/mac/bounces.d.ts +12 -27
  21. package/dist/mac/bounces.js +96 -117
  22. package/dist/mac/bounces.js.map +1 -1
  23. package/dist/mac/cleanup.d.ts +11 -25
  24. package/dist/mac/cleanup.js +44 -107
  25. package/dist/mac/cleanup.js.map +1 -1
  26. package/dist/mac/drafts.d.ts +15 -34
  27. package/dist/mac/drafts.js +113 -186
  28. package/dist/mac/drafts.js.map +1 -1
  29. package/dist/mac/folders.d.ts +12 -21
  30. package/dist/mac/folders.js +44 -59
  31. package/dist/mac/folders.js.map +1 -1
  32. package/dist/mac/index.d.ts +2 -22
  33. package/dist/mac/index.js +19 -81
  34. package/dist/mac/index.js.map +1 -1
  35. package/dist/mac/profile.d.ts +13 -0
  36. package/dist/mac/profile.js +149 -0
  37. package/dist/mac/profile.js.map +1 -0
  38. package/dist/mac/read.d.ts +21 -37
  39. package/dist/mac/read.js +198 -216
  40. package/dist/mac/read.js.map +1 -1
  41. package/dist/mac/run.d.ts +51 -38
  42. package/dist/mac/run.js +138 -103
  43. package/dist/mac/run.js.map +1 -1
  44. package/dist/mac/scripts.d.ts +90 -91
  45. package/dist/mac/scripts.js +167 -126
  46. package/dist/mac/scripts.js.map +1 -1
  47. package/dist/mac/send.d.ts +10 -20
  48. package/dist/mac/send.js +99 -89
  49. package/dist/mac/send.js.map +1 -1
  50. package/dist/mac/signatures.d.ts +2 -2
  51. package/dist/mac/signatures.js +9 -11
  52. package/dist/mac/signatures.js.map +1 -1
  53. package/dist/mac/templates.d.ts +14 -26
  54. package/dist/mac/templates.js +79 -120
  55. package/dist/mac/templates.js.map +1 -1
  56. package/dist/mail.d.ts +80 -46
  57. package/dist/mail.js +179 -73
  58. package/dist/mail.js.map +1 -1
  59. package/dist/runtime.d.ts +76 -43
  60. package/dist/runtime.js +103 -48
  61. package/dist/runtime.js.map +1 -1
  62. package/dist/shared/args.d.ts +48 -0
  63. package/dist/shared/args.js +216 -0
  64. package/dist/shared/args.js.map +1 -0
  65. package/dist/shared/attachmentMatch.d.ts +26 -20
  66. package/dist/shared/attachmentMatch.js +60 -23
  67. package/dist/shared/attachmentMatch.js.map +1 -1
  68. package/dist/shared/bounceReport.d.ts +11 -0
  69. package/dist/shared/bounceReport.js +45 -0
  70. package/dist/shared/bounceReport.js.map +1 -0
  71. package/dist/shared/bounceRules.d.ts +38 -10
  72. package/dist/shared/bounceRules.js +50 -32
  73. package/dist/shared/bounceRules.js.map +1 -1
  74. package/dist/shared/exec.d.ts +21 -0
  75. package/dist/shared/exec.js +47 -0
  76. package/dist/shared/exec.js.map +1 -0
  77. package/dist/shared/json.d.ts +17 -6
  78. package/dist/shared/json.js +35 -24
  79. package/dist/shared/json.js.map +1 -1
  80. package/dist/shared/locate.d.ts +31 -0
  81. package/dist/shared/locate.js +128 -0
  82. package/dist/shared/locate.js.map +1 -0
  83. package/dist/shared/replyBody.d.ts +31 -36
  84. package/dist/shared/replyBody.js +83 -72
  85. package/dist/shared/replyBody.js.map +1 -1
  86. package/dist/shared/templateLimits.d.ts +14 -0
  87. package/dist/shared/templateLimits.js +21 -0
  88. package/dist/shared/templateLimits.js.map +1 -0
  89. package/dist/templateBody.d.ts +69 -0
  90. package/dist/{outlookTemplateSections.js → templateBody.js} +80 -73
  91. package/dist/templateBody.js.map +1 -0
  92. package/dist/types.d.ts +381 -168
  93. package/dist/types.js +8 -9
  94. package/dist/types.js.map +1 -1
  95. package/dist/windows/accounts.d.ts +7 -12
  96. package/dist/windows/accounts.js +19 -34
  97. package/dist/windows/accounts.js.map +1 -1
  98. package/dist/windows/attachments.d.ts +9 -24
  99. package/dist/windows/attachments.js +94 -119
  100. package/dist/windows/attachments.js.map +1 -1
  101. package/dist/windows/bounces.d.ts +13 -26
  102. package/dist/windows/bounces.js +168 -200
  103. package/dist/windows/bounces.js.map +1 -1
  104. package/dist/windows/cleanup.d.ts +23 -23
  105. package/dist/windows/cleanup.js +99 -113
  106. package/dist/windows/cleanup.js.map +1 -1
  107. package/dist/windows/drafts.d.ts +16 -35
  108. package/dist/windows/drafts.js +156 -253
  109. package/dist/windows/drafts.js.map +1 -1
  110. package/dist/windows/folders.d.ts +8 -13
  111. package/dist/windows/folders.js +44 -52
  112. package/dist/windows/folders.js.map +1 -1
  113. package/dist/windows/index.d.ts +2 -27
  114. package/dist/windows/index.js +11 -66
  115. package/dist/windows/index.js.map +1 -1
  116. package/dist/windows/read.d.ts +24 -60
  117. package/dist/windows/read.js +265 -306
  118. package/dist/windows/read.js.map +1 -1
  119. package/dist/windows/run.d.ts +38 -40
  120. package/dist/windows/run.js +151 -112
  121. package/dist/windows/run.js.map +1 -1
  122. package/dist/windows/scripts.d.ts +65 -55
  123. package/dist/windows/scripts.js +109 -85
  124. package/dist/windows/scripts.js.map +1 -1
  125. package/dist/windows/send.d.ts +9 -9
  126. package/dist/windows/send.js +76 -88
  127. package/dist/windows/send.js.map +1 -1
  128. package/dist/windows/signatures.d.ts +4 -10
  129. package/dist/windows/signatures.js +30 -37
  130. package/dist/windows/signatures.js.map +1 -1
  131. package/dist/windows/templates.d.ts +12 -18
  132. package/dist/windows/templates.js +117 -150
  133. package/dist/windows/templates.js.map +1 -1
  134. package/package.json +5 -4
  135. package/dist/OutlookService.d.ts +0 -43
  136. package/dist/OutlookService.js +0 -137
  137. package/dist/OutlookService.js.map +0 -1
  138. package/dist/outlookTemplateSections.d.ts +0 -62
  139. package/dist/outlookTemplateSections.js.map +0 -1
  140. package/dist/shared/capabilities.d.ts +0 -3
  141. package/dist/shared/capabilities.js +0 -9
  142. package/dist/shared/capabilities.js.map +0 -1
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.cleanUndeliverableEmails = cleanUndeliverableEmails;
4
4
  exports.collectBouncedRecipients = collectBouncedRecipients;
5
5
  exports.readSentRecipientGroups = readSentRecipientGroups;
6
- // Bounce-backs: finding them, filing them away, and mining them for the
6
+ // Bounce-backs: finding them, clearing them away, and mining them for the
7
7
  // addresses that failed.
8
8
  const run_1 = require("./run");
9
9
  const scripts_1 = require("./scripts");
@@ -11,257 +11,225 @@ const json_1 = require("../shared/json");
11
11
  const bounceRules_1 = require("../shared/bounceRules");
12
12
  const mail_1 = require("../mail");
13
13
  /**
14
- * The classifier lists, generated from the shared rules so Windows and macOS
15
- * cannot come to disagree about what a bounce is. Emitted once per script,
16
- * before the item loop.
17
- */
18
- const BOUNCE_LISTS_PS = `
19
- $phrases = @(${(0, run_1.psList)(bounceRules_1.BOUNCE_SUBJECT_PHRASES)})
20
- $daemonAddr = @(${(0, run_1.psList)(bounceRules_1.BOUNCE_DAEMON_ADDRESSES)})
21
- $daemonName = @(${(0, run_1.psList)(bounceRules_1.BOUNCE_DAEMON_NAMES)})
22
- `;
23
- /**
24
- * Per-item classifier. Given an Outlook item in `$item` and the account address
25
- * (lowercased) in `$targetLc`, sets `$reason` to why it's a bounce ('' if not)
26
- * and fills `$failedRcpts` with the recipient addresses parsed from the bounce
27
- * body. Also leaves `$subj`/`$sName`/`$sEmail` populated for the caller's report.
14
+ * The classifier, generated from the shared rules so Windows and macOS can't
15
+ * disagree about what a bounce is. Defines `Get-BounceReport`, which returns
16
+ * null for an item that isn't a bounce and a report row for one that is.
28
17
  *
29
- * The body is only read once a cheaper signal (class/sender/subject) has already
18
+ * The body is read only after a cheaper signal (class, sender, subject) has
30
19
  * matched — that ordering is what keeps a large-inbox scan affordable.
31
20
  */
32
- const BOUNCE_CLASSIFY_PS = `
33
- $reason = ''
34
- $failedRcpts = @()
35
- $subj = ''
36
- $sName = ''
37
- $sEmail = ''
38
- $cls = 0
39
- try { $cls = [int]$item.Class } catch {}
40
- if ($cls -eq 43 -or $cls -eq 46) {
41
- try { if ($item.Subject) { $subj = [string]$item.Subject } } catch {}
42
- $subjLc = $subj.ToLower()
43
- try { if ($item.SenderName) { $sName = [string]$item.SenderName } } catch {}
44
- $sNameLc = $sName.ToLower()
45
- try { if ($item.SenderEmailAddress) { $sEmail = [string]$item.SenderEmailAddress } } catch {}
46
- $sEmailLc = $sEmail.ToLower()
47
- $msgClass = ''
48
- try { $msgClass = [string]$item.MessageClass } catch {}
49
- if ($msgClass -like '${bounceRules_1.NDR_MESSAGE_CLASS_PREFIX}*') { $reason = 'Non-delivery report (NDR)' }
50
- if ($reason -eq '') { foreach ($d in $daemonAddr) { if ($sEmailLc.Contains($d)) { $reason = 'From mail-delivery system'; break } } }
51
- if ($reason -eq '') { foreach ($d in $daemonName) { if ($sNameLc.Contains($d)) { $reason = 'From mail-delivery system'; break } } }
52
- if ($reason -eq '') { foreach ($p in $phrases) { if ($subjLc.Contains($p)) { $reason = "Bounce subject phrase: '$p'"; break } } }
53
- if ($reason -ne '') {
54
- $bodyRaw = ''
55
- try { if ($item.Body) { $bodyRaw = [string]$item.Body } } catch {}
56
- if ($bodyRaw -ne '') {
57
- $mm = [regex]::Matches($bodyRaw, '${bounceRules_1.BODY_ADDRESS_PATTERN}')
58
- foreach ($m in $mm) {
59
- $addr = $m.Value.ToLower().TrimEnd('.')
60
- if ($addr -eq $targetLc) { continue }
61
- if ($addr.Contains('mailer-daemon') -or $addr.Contains('postmaster') -or $addr.Contains('mail-daemon')) { continue }
62
- if ($failedRcpts -notcontains $addr) { $failedRcpts += $addr }
63
- if ($failedRcpts.Count -ge 5) { break }
64
- }
65
- }
21
+ function bounceClassifierScript() {
22
+ return `
23
+ $bouncePhrases = ${(0, run_1.psArray)(bounceRules_1.BOUNCE_SUBJECT_PHRASES)}
24
+ $daemonAddresses = ${(0, run_1.psArray)(bounceRules_1.BOUNCE_DAEMON_ADDRESSES)}
25
+ $daemonNames = ${(0, run_1.psArray)(bounceRules_1.BOUNCE_DAEMON_NAMES)}
26
+ $daemonSelf = ${(0, run_1.psArray)(bounceRules_1.DAEMON_SELF_ADDRESSES)}
27
+ $ndrPrefix = ${(0, run_1.psString)(bounceRules_1.NDR_MESSAGE_CLASS_PREFIX)}
28
+ $addressPattern = ${(0, run_1.psString)(bounceRules_1.BODY_ADDRESS_PATTERN)}
29
+ $maxFailed = ${(0, run_1.psInt)(bounceRules_1.MAX_FAILED_RECIPIENTS)}
30
+ $accountLower = $target.ToLower()
31
+ function Get-BounceReport($item) {
32
+ $class = 0
33
+ try { $class = [int]$item.Class } catch {}
34
+ if ($class -ne 43 -and $class -ne 46) { return $null }
35
+ $subject = ''
36
+ try { $subject = [string]$item.Subject } catch {}
37
+ $senderName = ''
38
+ try { $senderName = [string]$item.SenderName } catch {}
39
+ $senderEmail = ''
40
+ try { $senderEmail = [string]$item.SenderEmailAddress } catch {}
41
+ $messageClass = ''
42
+ try { $messageClass = [string]$item.MessageClass } catch {}
43
+ $reason = ''
44
+ if ($messageClass.StartsWith($ndrPrefix, [StringComparison]::OrdinalIgnoreCase)) { $reason = ${(0, run_1.psString)(bounceRules_1.BOUNCE_REASON.ndr)} }
45
+ if (-not $reason) { foreach ($d in $daemonAddresses) { if ($senderEmail.ToLower().Contains($d)) { $reason = ${(0, run_1.psString)(bounceRules_1.BOUNCE_REASON.daemon)}; break } } }
46
+ if (-not $reason) { foreach ($d in $daemonNames) { if ($senderName.ToLower().Contains($d)) { $reason = ${(0, run_1.psString)(bounceRules_1.BOUNCE_REASON.daemon)}; break } } }
47
+ if (-not $reason) { foreach ($p in $bouncePhrases) { if ($subject.ToLower().Contains($p)) { $reason = "${bounceRules_1.BOUNCE_REASON.subjectPhrase('$p')}"; break } } }
48
+ if (-not $reason) { return $null }
49
+ $failedRecipients = @()
50
+ $body = ''
51
+ try { $body = [string]$item.Body } catch {}
52
+ foreach ($m in [regex]::Matches($body, $addressPattern)) {
53
+ $address = $m.Value.ToLower().TrimEnd('.')
54
+ if ($address -eq $accountLower) { continue }
55
+ $isDaemon = $false
56
+ foreach ($d in $daemonSelf) { if ($address.Contains($d)) { $isDaemon = $true; break } }
57
+ if ($isDaemon -or $failedRecipients -contains $address) { continue }
58
+ $failedRecipients += $address
59
+ if ($failedRecipients.Count -ge $maxFailed) { break }
60
+ }
61
+ $received = ''
62
+ try { $received = $item.ReceivedTime.ToString('yyyy-MM-dd HH:mm') } catch {}
63
+ return [PSCustomObject]@{
64
+ entryId = [string]$item.EntryID
65
+ subject = $subject.Trim()
66
+ senderName = $senderName
67
+ senderEmail = $senderEmail
68
+ receivedTime = $received
69
+ matchedReason = $reason
70
+ failedRecipients = @($failedRecipients)
66
71
  }
67
72
  }
68
73
  `;
74
+ }
69
75
  /**
70
- * Scan an account's inbox for bounce-back / non-delivery messages — Outlook NDRs,
71
- * mail-daemon/postmaster rejections, and "Message blocked"-style Google/O365
72
- * failure notices — and, unless previewing, move each to Deleted Items (recoverable).
76
+ * Scan an account's Inbox for bounce-backs and, unless dry-running, move each to
77
+ * Deleted Items (recoverable).
73
78
  *
74
- * Classification is deliberately conservative so ordinary mail that merely
75
- * mentions "delivery" is never caught: an item matches only when its
76
- * MessageClass is an NDR report, its sender fingerprints as a mail-delivery
77
- * daemon/postmaster, or its subject contains a specific bounce phrase.
78
- *
79
- * Matched item references are snapshotted before any Delete() call — deleting
80
- * mutates the folder collection, so deleting mid-enumeration would skip items
81
- * (same pattern as sendAllDrafts).
79
+ * Matched items are collected before the first Delete(): deleting shifts the
80
+ * collection, and deleting mid-enumeration would skip items.
82
81
  */
83
- async function cleanUndeliverableEmails(emailAccount, daysBack = 30, dryRun = true) {
84
- (0, run_1.requireWindows)();
85
- const days = (0, mail_1.clamp)(daysBack, 1, 365);
86
- const script = `${(0, scripts_1.accountScript)(emailAccount)}
87
- $dryRun = ${(0, run_1.psBool)(dryRun)}
88
- ${(0, scripts_1.namedStoreScript)(emailAccount)}
89
- $inbox = $store.GetDefaultFolder(6)
90
- $cutoff = (Get-Date).AddDays(-${days}).ToString('MM/dd/yyyy HH:mm')
91
- $filtered = $inbox.Items.Restrict("[ReceivedTime] >= '$cutoff'")
92
- ${BOUNCE_LISTS_PS}
93
- $targetLc = $target.ToLower()
94
- $matched = @() # snapshot of COM item refs to (optionally) delete afterward
95
- $report = @()
82
+ async function cleanUndeliverableEmails(request) {
83
+ const output = await (0, run_1.runPowerShellJson)(`${(0, scripts_1.accountScript)(request.account)}
84
+ ${scripts_1.NAMED_STORE_PS}
85
+ ${bounceClassifierScript()}
86
+ $dryRun = ${(0, run_1.psBool)(request.dryRun)}
87
+ $inbox = $store.GetDefaultFolder(${(0, run_1.psInt)(mail_1.FolderId.Inbox)})
88
+ ${(0, scripts_1.cutoffScript)(request.daysBack)}
89
+ ${(0, scripts_1.itemsSinceScript)('inbox', 'ReceivedTime')}
90
+ $reports = @()
91
+ $matchedItems = @()
96
92
  $count = $filtered.Count
97
93
  for ($i = 1; $i -le $count; $i++) {
98
94
  $item = $null
99
95
  try { $item = $filtered.Item($i) } catch { continue }
100
96
  if ($item -eq $null) { continue }
101
- ${BOUNCE_CLASSIFY_PS}
102
- if ($reason -eq '') { continue }
103
-
104
- $rt = ''
105
- try { $rt = $item.ReceivedTime.ToString('yyyy-MM-dd HH:mm') } catch {}
106
- $report += [PSCustomObject]@{
107
- entryId = $item.EntryID
108
- subject = $subj.Trim()
109
- senderName = $sName
110
- senderEmail = $sEmail
111
- receivedTime = $rt
112
- matchedReason = $reason
113
- failedRecipients = @($failedRcpts)
114
- }
115
- $matched += $item
97
+ $received = $null
98
+ try { $received = $item.ReceivedTime } catch {}
99
+ if ($received -ne $null -and $received -lt $cutoff) { continue }
100
+ $report = Get-BounceReport $item
101
+ if ($report -eq $null) { continue }
102
+ $reports += $report
103
+ $matchedItems += $item
116
104
  }
117
-
118
105
  $deleted = 0
119
106
  $failed = @()
120
107
  if (-not $dryRun) {
121
- foreach ($m in $matched) {
122
- $s = ''
123
- try { $s = [string]$m.Subject } catch {}
108
+ foreach ($m in $matchedItems) {
109
+ $entryId = ''
110
+ $subject = ''
111
+ try { $entryId = [string]$m.EntryID } catch {}
112
+ try { $subject = [string]$m.Subject } catch {}
124
113
  try { $m.Delete(); $deleted++ }
125
- catch { $failed += [PSCustomObject]@{ subject = $s; error = $_.Exception.Message } }
114
+ catch { ${(0, json_1.itemFailurePs)('$entryId')} }
126
115
  }
127
116
  }
128
-
129
- [PSCustomObject]@{
130
- account = $target
131
- scannedDays = ${days}
132
- dryRun = $dryRun
133
- matchedCount = $report.Count
117
+ ConvertTo-Json -Depth 4 -InputObject ([PSCustomObject]@{
134
118
  deletedCount = $deleted
135
- matched = @($report)
136
- failed = @($failed)
137
- } | ConvertTo-Json -Depth 4
138
- `;
139
- const parsed = (0, json_1.parseObject)(await (0, run_1.runPowerShell)(script, 300000));
119
+ matched = @($reports)
120
+ failed = @($failed)
121
+ })
122
+ `, 'scan');
123
+ const e = (0, json_1.record)(output);
124
+ const matched = (0, json_1.toArray)(e.matched).map(row => {
125
+ const m = (0, json_1.record)(row);
126
+ return {
127
+ entryId: (0, json_1.str)(m.entryId),
128
+ subject: (0, json_1.str)(m.subject),
129
+ senderName: (0, json_1.str)(m.senderName),
130
+ senderEmail: (0, json_1.str)(m.senderEmail),
131
+ receivedTime: (0, json_1.str)(m.receivedTime),
132
+ matchedReason: (0, json_1.str)(m.matchedReason),
133
+ failedRecipients: (0, json_1.strList)(m.failedRecipients),
134
+ };
135
+ });
140
136
  return {
141
- account: (0, json_1.str)(parsed.account) || emailAccount,
142
- scannedDays: (0, json_1.num)(parsed.scannedDays) || days,
143
- dryRun: parsed.dryRun !== false,
144
- matchedCount: (0, json_1.num)(parsed.matchedCount),
145
- deletedCount: (0, json_1.num)(parsed.deletedCount),
146
- matched: (0, json_1.toArray)(parsed.matched).map(m => {
147
- const e = (0, json_1.record)(m);
148
- return {
149
- entryId: (0, json_1.str)(e.entryId),
150
- subject: (0, json_1.str)(e.subject),
151
- senderName: (0, json_1.str)(e.senderName),
152
- senderEmail: (0, json_1.str)(e.senderEmail),
153
- receivedTime: (0, json_1.str)(e.receivedTime),
154
- matchedReason: (0, json_1.str)(e.matchedReason),
155
- failedRecipients: (0, json_1.toArray)(e.failedRecipients).map(json_1.str),
156
- };
157
- }),
158
- failed: (0, json_1.toArray)(parsed.failed).map(f => {
159
- const e = (0, json_1.record)(f);
160
- return { subject: (0, json_1.str)(e.subject), error: (0, json_1.str)(e.error) };
161
- }),
137
+ account: request.account,
138
+ scannedDays: request.daysBack,
139
+ dryRun: request.dryRun,
140
+ matchedCount: matched.length,
141
+ deletedCount: (0, json_1.num)(e.deletedCount),
142
+ matched,
143
+ failed: (0, json_1.itemFailures)(e.failed),
162
144
  };
163
145
  }
164
146
  /**
165
- * Read-only scan of an account's Inbox (and, when scanDeleted, its Deleted Items)
166
- * for bounce messages, returning just the deduped set of failed recipient
167
- * addresses. Uses the same conservative classifier as cleanUndeliverableEmails.
168
- *
169
- * Deleted Items is included so this still works after cleanUndeliverableEmails has
170
- * already filed the bounces there — the blacklist step doesn't depend on running
171
- * before the cleanup. This never deletes anything.
147
+ * The deduplicated addresses bounce-backs report as failed, from the Inbox and,
148
+ * optionally, Deleted Items — so it still works after the bounces were cleaned
149
+ * away. Never deletes anything.
172
150
  */
173
- async function collectBouncedRecipients(emailAccount, daysBack = 30, scanDeleted = true) {
174
- if (process.platform !== 'win32')
175
- return [];
176
- const days = (0, mail_1.clamp)(daysBack, 1, 365);
177
- // 6 = Inbox, 3 = Deleted Items.
178
- const folderIds = scanDeleted ? '@(6, 3)' : '@(6)';
179
- const script = `${(0, scripts_1.accountScript)(emailAccount)}
151
+ async function collectBouncedRecipients(request) {
152
+ const roles = request.includeDeletedItems ? [mail_1.FolderId.Inbox, mail_1.FolderId.DeletedItems] : [mail_1.FolderId.Inbox];
153
+ const output = await (0, run_1.runPowerShellJson)(`${(0, scripts_1.accountScript)(request.account)}
180
154
  ${scripts_1.DELIVERY_STORE_PS}
181
- $cutoff = (Get-Date).AddDays(-${days}).ToString('MM/dd/yyyy HH:mm')
182
- ${BOUNCE_LISTS_PS}
183
- $targetLc = $target.ToLower()
184
- $found = @{}
185
- foreach ($fid in ${folderIds}) {
186
- $folder = $null
187
- try { $folder = $store.GetDefaultFolder($fid) } catch {}
188
- if ($folder -eq $null) { continue }
189
- $filtered = $folder.Items.Restrict("[ReceivedTime] >= '$cutoff'")
155
+ ${bounceClassifierScript()}
156
+ ${(0, scripts_1.cutoffScript)(request.daysBack)}
157
+ $found = [ordered]@{}
158
+ foreach ($role in @(${roles.map(run_1.psInt).join(', ')})) {
159
+ $scanFolder = $null
160
+ try { $scanFolder = $store.GetDefaultFolder($role) } catch {}
161
+ if ($scanFolder -eq $null) { continue }
162
+ ${(0, scripts_1.itemsSinceScript)('scanFolder', 'ReceivedTime').trim()}
190
163
  $count = $filtered.Count
191
164
  for ($i = 1; $i -le $count; $i++) {
192
165
  $item = $null
193
166
  try { $item = $filtered.Item($i) } catch { continue }
194
167
  if ($item -eq $null) { continue }
195
- ${BOUNCE_CLASSIFY_PS}
196
- if ($reason -eq '') { continue }
197
- foreach ($a in $failedRcpts) { $found[$a] = $true }
168
+ $received = $null
169
+ try { $received = $item.ReceivedTime } catch {}
170
+ if ($received -ne $null -and $received -lt $cutoff) { continue }
171
+ $report = Get-BounceReport $item
172
+ if ($report -eq $null) { continue }
173
+ foreach ($address in $report.failedRecipients) { $found[$address] = $true }
198
174
  }
199
175
  }
200
- ConvertTo-Json @($found.Keys) -Depth 2
201
- `;
202
- return (0, json_1.parseArray)(await (0, run_1.runPowerShell)(script, 300000)).map(json_1.str).filter(Boolean);
176
+ ConvertTo-Json -Compress -InputObject @($found.Keys)
177
+ `, 'scan');
178
+ return (0, json_1.strList)(output);
203
179
  }
204
180
  /**
205
- * Read the account's Sent Items within the window, returning each mail with the
206
- * full SMTP address set it was sent to (To + CC + BCC), newest first. This is how
207
- * the "was every address for this contact tried?" question gets answered: where a
208
- * blast sends one email per organization addressed to all of its addresses, a sent
209
- * message's recipient set IS that organization's full address set.
181
+ * Sent Items within the window, each message with the full SMTP address set it
182
+ * went to (To + CC + BCC), newest first.
210
183
  */
211
- async function readSentRecipientGroups(emailAccount, daysBack = 30, limit = 3000) {
212
- if (process.platform !== 'win32')
213
- return [];
214
- const days = (0, mail_1.clamp)(daysBack, 1, 365);
215
- const cap = (0, mail_1.clamp)(limit, 1, 10000);
216
- const script = `${(0, scripts_1.accountScript)(emailAccount)}
184
+ async function readSentRecipientGroups(request) {
185
+ const output = await (0, run_1.runPowerShellJson)(`${(0, scripts_1.accountScript)(request.account)}
217
186
  ${scripts_1.DELIVERY_STORE_PS}
218
- $sent = $store.GetDefaultFolder(5) # olFolderSentMail
219
- $cutoff = (Get-Date).AddDays(-${days}).ToString('MM/dd/yyyy HH:mm')
220
- $items = $sent.Items.Restrict("[SentOn] >= '$cutoff'")
221
- $items.Sort('[SentOn]', $true)
222
- $results = @()
223
- $count = [Math]::Min($items.Count, ${cap})
224
- for ($i = 1; $i -le $count; $i++) {
187
+ $sentFolder = $store.GetDefaultFolder(${(0, run_1.psInt)(mail_1.FolderId.SentMail)})
188
+ ${(0, scripts_1.cutoffScript)(request.daysBack)}
189
+ ${(0, scripts_1.itemsSinceScript)('sentFolder', 'SentOn')}
190
+ $filtered.Sort('[SentOn]', $true)
191
+ $limit = ${(0, run_1.psInt)(request.limit)}
192
+ $rows = @()
193
+ $count = $filtered.Count
194
+ for ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {
225
195
  $m = $null
226
- try { $m = $items.Item($i) } catch { continue }
196
+ try { $m = $filtered.Item($i) } catch { continue }
227
197
  if ($m -eq $null) { continue }
228
- $cls = 0
229
- try { $cls = [int]$m.Class } catch {}
230
- if ($cls -ne 43) { continue } # olMail only
231
- $addrs = @()
198
+ $sentOn = $null
199
+ try { $sentOn = $m.SentOn } catch {}
200
+ if ($sentOn -ne $null -and $sentOn -lt $cutoff) { break }
201
+ $class = 0
202
+ try { $class = [int]$m.Class } catch {}
203
+ if ($class -ne 43) { continue }
204
+ $addresses = @()
232
205
  try {
233
206
  foreach ($r in $m.Recipients) {
234
- $addr = ''
235
- try { $addr = [string]$r.Address } catch {}
236
- # Internal recipients resolve to an Exchange DN, not SMTP — recover the
237
- # real address so external correlation still works.
238
- if ($addr -notlike '*@*') {
239
- try { $addr = [string]$r.AddressEntry.GetExchangeUser().PrimarySmtpAddress } catch {}
207
+ $address = ''
208
+ try { $address = [string]$r.Address } catch {}
209
+ # An internal recipient resolves to an Exchange DN; recover its SMTP address.
210
+ if ($address -notlike '*@*') {
211
+ try { $address = [string]$r.AddressEntry.GetExchangeUser().PrimarySmtpAddress } catch {}
240
212
  }
241
- if ($addr -like '*@*') { $addrs += $addr.ToLower() }
213
+ if ($address -like '*@*') { $addresses += $address.ToLower() }
242
214
  }
243
215
  } catch {}
244
- if ($addrs.Count -eq 0) { continue }
245
- $sentOn = ''
246
- try { $sentOn = $m.SentOn.ToString('yyyy-MM-dd HH:mm') } catch {}
247
- $subj = ''
248
- try { if ($m.Subject) { $subj = [string]$m.Subject } } catch {}
249
- $results += [PSCustomObject]@{
250
- entryId = $m.EntryID
251
- subject = $subj.Trim()
252
- sentOn = $sentOn
253
- recipients = @($addrs)
216
+ if ($addresses.Count -eq 0) { continue }
217
+ $rows += [PSCustomObject]@{
218
+ entryId = [string]$m.EntryID
219
+ subject = ([string]$m.Subject).Trim()
220
+ sentOn = if ($sentOn -ne $null) { $sentOn.ToString('yyyy-MM-dd HH:mm') } else { '' }
221
+ recipients = @($addresses)
254
222
  }
255
223
  }
256
- ConvertTo-Json @($results) -Depth 4
257
- `;
258
- return (0, json_1.parseArray)(await (0, run_1.runPowerShell)(script, 300000)).map(it => {
259
- const e = (0, json_1.record)(it);
224
+ ConvertTo-Json -Depth 4 -InputObject @($rows)
225
+ `, 'scan');
226
+ return (0, json_1.toArray)(output).map(row => {
227
+ const e = (0, json_1.record)(row);
260
228
  return {
261
229
  entryId: (0, json_1.str)(e.entryId),
262
230
  subject: (0, json_1.str)(e.subject),
263
231
  sentOn: (0, json_1.str)(e.sentOn),
264
- recipients: (0, json_1.toArray)(e.recipients).map(json_1.str),
232
+ recipients: (0, json_1.strList)(e.recipients),
265
233
  };
266
234
  });
267
235
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bounces.js","sourceRoot":"","sources":["../../src/windows/bounces.ts"],"names":[],"mappings":"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"}
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"]}
@@ -1,29 +1,29 @@
1
- import type { DeleteMailOptions, DeleteMailResult, PurgeDeletedItemsResult } from '../types';
1
+ import type { DeleteEmailsRequest, PurgeRequest } from '../backend';
2
+ import type { DeleteMailResult, PurgeDeletedItemsResult } from '../types';
2
3
  /**
3
- * Delete mail by EntryID from anywhere in the account. Delete() moves each item to
4
- * Deleted Items, so this is recoverable — purgeDeletedItems is what destroys.
4
+ * Delete mail by entry id from anywhere in the account. Delete() moves each item
5
+ * to Deleted Items, so this is recoverable — purgeDeletedItems is what destroys.
5
6
  *
6
- * ⚠️ This is the one tool here that can reach received mail, and EntryIDs are a
7
- * genuinely unsafe key for it: GetItemFromID silently returns a DIFFERENT message
8
- * when handed a stale or wrong id, which is common precisely when many replies share
9
- * one subject (see saveEmailAttachmentDetailed). Three things hold the line:
10
- *
11
- * - **Inbox and Sent Items are refused by default, INCLUDING their subfolders** —
12
- * a filed subfolder is still received mail. `allowProtected` lifts that, and is
13
- * the caller explicitly taking responsibility.
14
- * - **`dryRun` resolves and reports without deleting**, so the exact subjects and
15
- * folders can be shown to the user before anything happens. Use it first.
16
- * - **Every outcome echoes the subject and folderPath** of the item actually
17
- * resolved, so a wrong id is visible after the fact rather than silent.
7
+ * This is the one operation that can reach received mail, and an entry id is an
8
+ * unsafe key for it: a stale or wrong id can resolve to a DIFFERENT message.
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.
14
+ * - Inbox and Sent Items are refused, INCLUDING their subfolders (a filed
15
+ * folder is still received mail), unless `allowProtected`.
16
+ * - `dryRun` resolves and reports without deleting, so the exact subjects and
17
+ * folders can be checked before anything happens.
18
+ * - Every outcome echoes the subject and folder of the item actually resolved.
18
19
  */
19
- export declare function deleteOutlookEmails(emailAccount: string, entryIds: string[], options?: DeleteMailOptions): Promise<DeleteMailResult>;
20
+ export declare function deleteOutlookEmails(request: DeleteEmailsRequest): Promise<DeleteMailResult>;
20
21
  /**
21
- * Permanently remove items from the account's Deleted Items folder. This is the ONE
22
- * genuinely irreversible operation here — nothing recovers from it — which is why it
23
- * is folder-scoped rather than keyed on an EntryID: it can only ever destroy what the
24
- * user already threw away.
22
+ * Permanently remove items from the account's Deleted Items. The ONE irreversible
23
+ * operation here, which is why it is folder-scoped rather than keyed on ids: it
24
+ * can only ever destroy what was already thrown away.
25
25
  *
26
- * `olderThanDays` keeps recent items (0 = purge everything). Iterates backwards, as
27
- * deleting mutates the collection and a forward walk would skip every other item.
26
+ * Walks backwards, since deleting shifts the collection and a forward walk would
27
+ * skip every other item.
28
28
  */
29
- export declare function purgeDeletedItems(emailAccount: string, olderThanDays?: number, dryRun?: boolean): Promise<PurgeDeletedItemsResult>;
29
+ export declare function purgeDeletedItems(request: PurgeRequest): Promise<PurgeDeletedItemsResult>;