@dawnlit/outlook-bridge 1.1.0 → 2.0.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 (139) hide show
  1. package/README.md +150 -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 +81 -43
  9. package/dist/errors.js +98 -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 +95 -112
  22. package/dist/mac/bounces.js.map +1 -1
  23. package/dist/mac/cleanup.d.ts +11 -25
  24. package/dist/mac/cleanup.js +50 -76
  25. package/dist/mac/cleanup.js.map +1 -1
  26. package/dist/mac/drafts.d.ts +13 -34
  27. package/dist/mac/drafts.js +95 -169
  28. package/dist/mac/drafts.js.map +1 -1
  29. package/dist/mac/folders.d.ts +10 -21
  30. package/dist/mac/folders.js +33 -47
  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 +18 -36
  39. package/dist/mac/read.js +154 -208
  40. package/dist/mac/read.js.map +1 -1
  41. package/dist/mac/run.d.ts +35 -39
  42. package/dist/mac/run.js +97 -102
  43. package/dist/mac/run.js.map +1 -1
  44. package/dist/mac/scripts.d.ts +78 -86
  45. package/dist/mac/scripts.js +125 -115
  46. package/dist/mac/scripts.js.map +1 -1
  47. package/dist/mac/send.d.ts +10 -20
  48. package/dist/mac/send.js +64 -86
  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 +75 -113
  55. package/dist/mac/templates.js.map +1 -1
  56. package/dist/mail.d.ts +77 -45
  57. package/dist/mail.js +166 -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 +28 -16
  66. package/dist/shared/attachmentMatch.js +60 -22
  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 +12 -6
  78. package/dist/shared/json.js +29 -23
  79. package/dist/shared/json.js.map +1 -1
  80. package/dist/shared/locate.d.ts +39 -0
  81. package/dist/shared/locate.js +129 -0
  82. package/dist/shared/locate.js.map +1 -0
  83. package/dist/shared/replyBody.d.ts +42 -32
  84. package/dist/shared/replyBody.js +84 -71
  85. package/dist/shared/replyBody.js.map +1 -1
  86. package/dist/templateBody.d.ts +63 -0
  87. package/dist/{outlookTemplateSections.js → templateBody.js} +72 -72
  88. package/dist/templateBody.js.map +1 -0
  89. package/dist/types.d.ts +360 -165
  90. package/dist/types.js +8 -9
  91. package/dist/types.js.map +1 -1
  92. package/dist/windows/accounts.d.ts +10 -1
  93. package/dist/windows/accounts.js +25 -13
  94. package/dist/windows/accounts.js.map +1 -1
  95. package/dist/windows/attachments.d.ts +9 -24
  96. package/dist/windows/attachments.js +94 -119
  97. package/dist/windows/attachments.js.map +1 -1
  98. package/dist/windows/bounces.d.ts +13 -26
  99. package/dist/windows/bounces.js +169 -200
  100. package/dist/windows/bounces.js.map +1 -1
  101. package/dist/windows/cleanup.d.ts +19 -23
  102. package/dist/windows/cleanup.js +93 -111
  103. package/dist/windows/cleanup.js.map +1 -1
  104. package/dist/windows/drafts.d.ts +16 -35
  105. package/dist/windows/drafts.js +167 -253
  106. package/dist/windows/drafts.js.map +1 -1
  107. package/dist/windows/folders.d.ts +8 -13
  108. package/dist/windows/folders.js +45 -52
  109. package/dist/windows/folders.js.map +1 -1
  110. package/dist/windows/index.d.ts +2 -27
  111. package/dist/windows/index.js +11 -65
  112. package/dist/windows/index.js.map +1 -1
  113. package/dist/windows/read.d.ts +24 -45
  114. package/dist/windows/read.js +266 -279
  115. package/dist/windows/read.js.map +1 -1
  116. package/dist/windows/run.d.ts +39 -39
  117. package/dist/windows/run.js +151 -111
  118. package/dist/windows/run.js.map +1 -1
  119. package/dist/windows/scripts.d.ts +71 -38
  120. package/dist/windows/scripts.js +123 -71
  121. package/dist/windows/scripts.js.map +1 -1
  122. package/dist/windows/send.d.ts +6 -11
  123. package/dist/windows/send.js +70 -79
  124. package/dist/windows/send.js.map +1 -1
  125. package/dist/windows/signatures.d.ts +4 -10
  126. package/dist/windows/signatures.js +30 -37
  127. package/dist/windows/signatures.js.map +1 -1
  128. package/dist/windows/templates.d.ts +12 -18
  129. package/dist/windows/templates.js +126 -151
  130. package/dist/windows/templates.js.map +1 -1
  131. package/package.json +5 -4
  132. package/dist/OutlookService.d.ts +0 -43
  133. package/dist/OutlookService.js +0 -137
  134. package/dist/OutlookService.js.map +0 -1
  135. package/dist/outlookTemplateSections.d.ts +0 -62
  136. package/dist/outlookTemplateSections.js.map +0 -1
  137. package/dist/shared/capabilities.d.ts +0 -3
  138. package/dist/shared/capabilities.js +0 -9
  139. 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,256 +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).
73
- *
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.
76
+ * Scan an account's Inbox for bounce-backs and, unless dry-running, move each to
77
+ * Deleted Items (recoverable).
78
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', 'filtered')}
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 { $failed += [PSCustomObject]@{ entryId = $entryId; subject = $subject; error = $_.Exception.Message } }
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', 'filtered').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)}
217
- $sent = $account.DeliveryStore.GetDefaultFolder(5) # olFolderSentMail
218
- $cutoff = (Get-Date).AddDays(-${days}).ToString('MM/dd/yyyy HH:mm')
219
- $items = $sent.Items.Restrict("[SentOn] >= '$cutoff'")
220
- $items.Sort('[SentOn]', $true)
221
- $results = @()
222
- $count = [Math]::Min($items.Count, ${cap})
223
- for ($i = 1; $i -le $count; $i++) {
184
+ async function readSentRecipientGroups(request) {
185
+ const output = await (0, run_1.runPowerShellJson)(`${(0, scripts_1.accountScript)(request.account)}
186
+ ${scripts_1.DELIVERY_STORE_PS}
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', 'filtered')}
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++) {
224
195
  $m = $null
225
- try { $m = $items.Item($i) } catch { continue }
196
+ try { $m = $filtered.Item($i) } catch { continue }
226
197
  if ($m -eq $null) { continue }
227
- $cls = 0
228
- try { $cls = [int]$m.Class } catch {}
229
- if ($cls -ne 43) { continue } # olMail only
230
- $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 = @()
231
205
  try {
232
206
  foreach ($r in $m.Recipients) {
233
- $addr = ''
234
- try { $addr = [string]$r.Address } catch {}
235
- # Internal recipients resolve to an Exchange DN, not SMTP — recover the
236
- # real address so external correlation still works.
237
- if ($addr -notlike '*@*') {
238
- 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 {}
239
212
  }
240
- if ($addr -like '*@*') { $addrs += $addr.ToLower() }
213
+ if ($address -like '*@*') { $addresses += $address.ToLower() }
241
214
  }
242
215
  } catch {}
243
- if ($addrs.Count -eq 0) { continue }
244
- $sentOn = ''
245
- try { $sentOn = $m.SentOn.ToString('yyyy-MM-dd HH:mm') } catch {}
246
- $subj = ''
247
- try { if ($m.Subject) { $subj = [string]$m.Subject } } catch {}
248
- $results += [PSCustomObject]@{
249
- entryId = $m.EntryID
250
- subject = $subj.Trim()
251
- sentOn = $sentOn
252
- 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)
253
222
  }
254
223
  }
255
- ConvertTo-Json @($results) -Depth 4
256
- `;
257
- return (0, json_1.parseArray)(await (0, run_1.runPowerShell)(script, 300000)).map(it => {
258
- 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);
259
228
  return {
260
229
  entryId: (0, json_1.str)(e.entryId),
261
230
  subject: (0, json_1.str)(e.subject),
262
231
  sentOn: (0, json_1.str)(e.sentOn),
263
- recipients: (0, json_1.toArray)(e.recipients).map(json_1.str),
232
+ recipients: (0, json_1.strList)(e.recipients),
264
233
  };
265
234
  });
266
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 { 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,29 +1,25 @@
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
+ * Three things hold the line:
10
+ * - Inbox and Sent Items are refused, INCLUDING their subfolders (a filed
11
+ * folder is still received mail), unless `allowProtected`.
12
+ * - `dryRun` resolves and reports without deleting, so the exact subjects and
13
+ * folders can be checked before anything happens.
14
+ * - Every outcome echoes the subject and folder of the item actually resolved.
18
15
  */
19
- export declare function deleteOutlookEmails(emailAccount: string, entryIds: string[], options?: DeleteMailOptions): Promise<DeleteMailResult>;
16
+ export declare function deleteOutlookEmails(request: DeleteEmailsRequest): Promise<DeleteMailResult>;
20
17
  /**
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.
18
+ * Permanently remove items from the account's Deleted Items. The ONE irreversible
19
+ * operation here, which is why it is folder-scoped rather than keyed on ids: it
20
+ * can only ever destroy what was already thrown away.
25
21
  *
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.
22
+ * Walks backwards, since deleting shifts the collection and a forward walk would
23
+ * skip every other item.
28
24
  */
29
- export declare function purgeDeletedItems(emailAccount: string, olderThanDays?: number, dryRun?: boolean): Promise<PurgeDeletedItemsResult>;
25
+ export declare function purgeDeletedItems(request: PurgeRequest): Promise<PurgeDeletedItemsResult>;