@dawnlit/outlook-bridge 1.1.1 → 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 -38
  39. package/dist/mac/read.js +154 -210
  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 -167
  90. package/dist/types.js +8 -9
  91. package/dist/types.js.map +1 -1
  92. package/dist/windows/accounts.d.ts +7 -12
  93. package/dist/windows/accounts.js +19 -34
  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 +168 -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 +92 -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 +44 -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 -66
  112. package/dist/windows/index.js.map +1 -1
  113. package/dist/windows/read.d.ts +24 -60
  114. package/dist/windows/read.js +261 -306
  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 +65 -55
  120. package/dist/windows/scripts.js +109 -85
  121. package/dist/windows/scripts.js.map +1 -1
  122. package/dist/windows/send.d.ts +6 -11
  123. package/dist/windows/send.js +68 -88
  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 +122 -149
  130. package/dist/windows/templates.js.map +1 -1
  131. package/package.json +4 -3
  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
@@ -13,291 +13,270 @@ const json_1 = require("../shared/json");
13
13
  const mail_1 = require("../mail");
14
14
  const errors_1 = require("../errors");
15
15
  /**
16
- * An arbitrary email body crosses the PowerShell boundary base64'd: it carries
17
- * quotes, control characters and non-ASCII that would otherwise have to survive
18
- * both the console codepage and JSON string escaping intact.
16
+ * The COM property carrying each kind of folder's timestamp. Sent Items and
17
+ * Drafts have no ReceivedTime, and filtering them on it returns an empty set
18
+ * that looks exactly like an empty folder.
19
19
  */
20
- function decodeBody(value) {
21
- return Buffer.from((0, json_1.str)(value), 'base64').toString('utf8');
20
+ function dateProperty(rootId) {
21
+ if (rootId === mail_1.FolderId.Drafts)
22
+ return 'LastModificationTime';
23
+ return (0, mail_1.isOutgoingRoot)(rootId) ? 'SentOn' : 'ReceivedTime';
22
24
  }
23
- // Outgoing roots keep their timestamp under a different COM property; anything
24
- // else carries ReceivedTime.
25
- const OUTGOING_DATE_PROPS = {
26
- [mail_1.WELL_KNOWN_FOLDERS['sent items']]: 'SentOn',
27
- [mail_1.WELL_KNOWN_FOLDERS.outbox]: 'SentOn',
28
- [mail_1.WELL_KNOWN_FOLDERS.drafts]: 'LastModificationTime',
29
- };
30
25
  /**
31
- * Read recent emails from the Outlook inbox for the given account.
32
- * No subject filtering — returns everything within the date window, up to the limit.
33
- * Does NOT download attachments; returns attachment names only.
34
- *
35
- * `folder` scopes the read to one folder under the Inbox instead of the Inbox
36
- * root. Without it, mail already filed away (Inbox\\Invoices) is unreachable —
37
- * which is the normal state of any mailbox its owner keeps tidy. Segments are
38
- * matched as direct children; a bare name that isn't a direct child falls back to
39
- * the same recursive by-name search moveOutlookEmails uses, so one folder string
40
- * works in both. Only the named folder is read — its own subfolders are not.
26
+ * Bodies cross the PowerShell boundary base64-encoded: they carry quotes,
27
+ * control characters and non-ASCII that would otherwise have to survive JSON
28
+ * escaping in two languages intact.
41
29
  */
42
- async function readInboxEmails(emailAccount, daysBack = 60, limit = 50, folder) {
43
- if (process.platform !== 'win32')
44
- return [];
45
- const ref = folder ? (0, mail_1.mailFolderRef)(folder) : { rootId: 6, rootLabel: 'Inbox', segments: [] };
46
- // A folder argument that trims away to nothing ("\\", " ") would otherwise
47
- // read the Inbox root and look like it had scoped — the exact silent
48
- // mis-scoping this parameter exists to prevent. A bare well-known name
49
- // ("Sent Items") legitimately has no segments, so it is not that case.
50
- if (folder && folder.trim() && ref.segments.length === 0 && ref.rootId === 6
51
- && !Object.prototype.hasOwnProperty.call(mail_1.WELL_KNOWN_FOLDERS, folder.trim().toLowerCase())) {
52
- throw new errors_1.NotFoundError('folder', `Folder '${folder}' does not name a folder under the Inbox.`);
53
- }
54
- // Which timestamp the folder's items actually carry (see the note in the script).
55
- const dateProp = OUTGOING_DATE_PROPS[ref.rootId] ?? 'ReceivedTime';
56
- const isOutgoing = (0, run_1.psBool)((0, mail_1.isOutgoingRoot)(ref.rootId));
57
- const script = `${(0, scripts_1.accountScript)(emailAccount)}
58
- ${(0, scripts_1.namedStoreScript)(emailAccount)}
59
- ${(0, scripts_1.mailScopeScript)(ref, folder || '')}
60
- $scopePath = $scope.FolderPath
61
- $cutoff = (Get-Date).AddDays(-${daysBack}).ToString('MM/dd/yyyy HH:mm')
62
- # Sent Items and Drafts carry no ReceivedTime, so restricting on it there returns
63
- # an empty set that looks exactly like an empty folder. Filter each on the date
64
- # property it actually has.
65
- $filtered = $scope.Items.Restrict("[${dateProp}] >= '$cutoff'")
30
+ const BODY_B64_PS = `[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes([string]$bodyRaw))`;
31
+ /** Recent mail from the Inbox root or one folder, newest first. */
32
+ async function readInboxEmails(request) {
33
+ const dateProp = dateProperty(request.folder.rootId);
34
+ const outgoing = (0, mail_1.isOutgoingRoot)(request.folder.rootId);
35
+ const output = await (0, run_1.runPowerShellJson)(`${(0, scripts_1.accountScript)(request.account)}
36
+ ${scripts_1.NAMED_STORE_PS}
37
+ ${(0, scripts_1.mailScopeScript)(request.folder, request.folderLabel)}
38
+ $scopePath = [string]$scope.FolderPath
39
+ ${(0, scripts_1.cutoffScript)(request.daysBack)}
40
+ ${(0, scripts_1.itemsSinceScript)('scope', dateProp, 'filtered')}
66
41
  $filtered.Sort('[${dateProp}]', $true)
67
- $results = @()
68
- $cap = [Math]::Min($filtered.Count, ${limit})
69
- for ($i = 1; $i -le $cap; $i++) {
70
- $item = $filtered.Item($i)
71
- $subj = if ($item.Subject) { $item.Subject.Trim() } else { '' }
72
- $bodyText = if ($item.Body) { $item.Body } else { '' }
73
- $bodyPreview = if ($bodyText.Length -gt 600) { $bodyText.Substring(0, 600) } else { $bodyText }
74
- $attNames = @()
75
- foreach ($att in $item.Attachments) { $attNames += $att.FileName }
76
- $stamp = ''
77
- try { $stamp = $item.${dateProp}.ToString('yyyy-MM-dd HH:mm') } catch {}
78
- # Outgoing mail has no meaningful sender line of its own — report who it is TO,
79
- # or a Sent Items listing reads as a folder of mail from yourself.
42
+ $limit = ${(0, run_1.psInt)(request.limit)}
43
+ $previewChars = ${(0, run_1.psInt)(request.previewChars)}
44
+ $rows = @()
45
+ $count = $filtered.Count
46
+ for ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {
47
+ $item = $null
48
+ try { $item = $filtered.Item($i) } catch {}
49
+ if ($item -eq $null) { continue }
50
+ $stamp = $null
51
+ try { $stamp = $item.${dateProp} } catch {}
52
+ # Sorted newest first, so the first item older than the window ends it.
53
+ if ($stamp -ne $null -and $stamp -lt $cutoff) { break }
54
+ $preview = ''
55
+ if ($previewChars -gt 0) {
56
+ $bodyText = ''
57
+ try { $bodyText = [string]$item.Body } catch {}
58
+ $preview = if ($bodyText.Length -gt $previewChars) { $bodyText.Substring(0, $previewChars) } else { $bodyText }
59
+ }
60
+ $attachmentNames = @()
61
+ try { foreach ($att in $item.Attachments) { $attachmentNames += [string]$att.FileName } } catch {}
80
62
  $who = ''
81
- try { $who = if ($item.To) { $item.To } else { '' } } catch {}
82
- $results += [PSCustomObject]@{
83
- entryId = $item.EntryID
84
- storeId = $storeId
85
- folderPath = $scopePath
86
- subject = $subj
87
- senderName = if (${isOutgoing}) { $who } elseif ($item.SenderName) { $item.SenderName } else { '' }
88
- senderEmail = if (${isOutgoing}) { $who } elseif ($item.SenderEmailAddress) { $item.SenderEmailAddress } else { '' }
89
- receivedTime = $stamp
90
- bodyPreview = $bodyPreview
91
- attachmentNames = $attNames
92
- attachmentCount = $item.Attachments.Count
63
+ $whoEmail = ''
64
+ if (${(0, run_1.psBool)(outgoing)}) {
65
+ # Outgoing mail reports who it went TO; "from" is always the mailbox itself.
66
+ try { $who = [string]$item.To } catch {}
67
+ $whoEmail = $who
68
+ } else {
69
+ try { $who = [string]$item.SenderName } catch {}
70
+ try { $whoEmail = [string]$item.SenderEmailAddress } catch {}
71
+ }
72
+ $rows += [PSCustomObject]@{
73
+ entryId = [string]$item.EntryID
74
+ storeId = $storeId
75
+ folderPath = $scopePath
76
+ subject = ([string]$item.Subject).Trim()
77
+ senderName = $who
78
+ senderEmail = $whoEmail
79
+ receivedTime = if ($stamp -ne $null) { $stamp.ToString('yyyy-MM-dd HH:mm') } else { '' }
80
+ bodyPreview = $preview
81
+ attachmentNames = @($attachmentNames)
93
82
  }
94
83
  }
95
- ConvertTo-Json $results -Depth 3
96
- `;
97
- return (0, json_1.parseArray)(await (0, run_1.runPowerShell)(script, 30000)).map(item => {
98
- const e = (0, json_1.record)(item);
84
+ ConvertTo-Json -Depth 4 -InputObject @($rows)
85
+ `, 'standard');
86
+ return (0, json_1.toArray)(output).map(row => {
87
+ const e = (0, json_1.record)(row);
88
+ const attachmentNames = (0, json_1.toArray)(e.attachmentNames).map(json_1.str);
99
89
  return {
100
90
  entryId: (0, json_1.str)(e.entryId),
101
91
  storeId: (0, json_1.str)(e.storeId),
102
- folderPath: (0, json_1.str)(e.folderPath),
103
92
  subject: (0, json_1.str)(e.subject),
104
93
  senderName: (0, json_1.str)(e.senderName),
105
94
  senderEmail: (0, json_1.str)(e.senderEmail),
106
95
  receivedTime: (0, json_1.str)(e.receivedTime),
107
96
  bodyPreview: (0, json_1.str)(e.bodyPreview),
108
- attachmentNames: (0, json_1.toArray)(e.attachmentNames).map(json_1.str),
109
- attachmentCount: typeof e.attachmentCount === 'number' ? e.attachmentCount : 0,
97
+ attachmentNames,
98
+ attachmentCount: attachmentNames.length,
99
+ folderPath: (0, json_1.str)(e.folderPath),
110
100
  };
111
101
  });
112
102
  }
113
103
  /**
114
- * Read one email's full plain-text body by EntryID.
115
- *
116
- * Uses `MailItem.Body`, not `HTMLBody`: Outlook's own plain-text rendering is
117
- * what every other reader here consumes, and the markup costs an order of
118
- * magnitude more for the same sentences. The tradeoff is that HTML tables
119
- * flatten, so figures broken out in a table lose their row/column pairing — read
120
- * those from the attachment where there is one.
104
+ * One email's full plain-text body, by id.
121
105
  *
122
- * Exists because `readInboxEmails` caps its preview at 600 chars, and a reply
123
- * whose first 600 chars read as complete can still carry a material detail below
124
- * the cut. Nothing detects that from the preview alone.
125
- *
126
- * `includeQuoted` is off by default — see splitQuotedOriginal for why the
127
- * quoted thread is a hazard to a caller reading figures out of a reply.
128
- * `quotedLength` is reported either way so a caller can tell it exists and ask again.
106
+ * `Body`, not `HTMLBody`: Outlook's own plain-text rendering is what every other
107
+ * reader here consumes, and markup costs an order of magnitude more for the same
108
+ * sentences. The tradeoff is that an HTML table's rows flatten.
129
109
  */
130
- async function readEmailBody(entryId, storeId, maxChars = 8000, includeQuoted = false) {
131
- (0, run_1.requireWindows)();
132
- const script = `
133
- $outlook = New-Object -ComObject Outlook.Application
134
- $ns = $outlook.GetNamespace('mapi')
135
- $ns.Logon()
136
- ${(0, scripts_1.getItemScript)(entryId, storeId)}
137
- if ($item -eq $null) { throw "Email not found for EntryID '${(0, run_1.psEscape)(entryId)}'" }
138
- $cls = 0
139
- try { $cls = [int]$item.Class } catch {}
140
- if ($cls -ne 43 -and $cls -ne 46) { throw "The item for this EntryID is not an email (Class=$cls). Re-run readInboxEmails for a current EntryID." }
110
+ async function readEmailBody(email) {
111
+ const output = await (0, run_1.runPowerShellJson)(`${scripts_1.SESSION_PS}
112
+ ${(0, scripts_1.itemLookupScript)(email)}
113
+ $class = 0
114
+ try { $class = [int]$item.Class } catch {}
115
+ # 43 = olMail, 46 = olReport (a non-delivery report is still worth reading).
116
+ if ($class -ne 43 -and $class -ne 46) {
117
+ throw "${(0, errors_1.failureTag)('NOT_FOUND', 'email')}The item with entry id '$lookupId' is not an email (class $class)."
118
+ }
141
119
  $bodyRaw = ''
142
- try { if ($item.Body) { $bodyRaw = [string]$item.Body } } catch {}
143
- $attNames = @()
144
- try { foreach ($att in $item.Attachments) { $attNames += $att.FileName } } catch {}
120
+ try { $bodyRaw = [string]$item.Body } catch {}
121
+ $attachmentNames = @()
122
+ try { foreach ($att in $item.Attachments) { $attachmentNames += [string]$att.FileName } } catch {}
145
123
  ${scripts_1.SENDER_SMTP_PS}
146
124
  $received = ''
147
125
  try { $received = $item.ReceivedTime.ToString('yyyy-MM-dd HH:mm') } catch {}
148
- $out = [PSCustomObject]@{
149
- entryId = $item.EntryID
150
- subject = if ($item.Subject) { $item.Subject.Trim() } else { '' }
151
- senderName = if ($item.SenderName) { $item.SenderName } else { '' }
126
+ ConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{
127
+ entryId = [string]$item.EntryID
128
+ subject = ([string]$item.Subject).Trim()
129
+ senderName = [string]$item.SenderName
152
130
  senderEmail = $senderSmtp
153
131
  receivedTime = $received
154
- body = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($bodyRaw))
155
- attachmentNames = $attNames
156
- }
157
- ConvertTo-Json $out -Depth 3 -Compress
158
- `;
159
- const raw = await (0, run_1.runPowerShell)(script, 20000);
160
- if (!raw || !raw.trim())
161
- throw new errors_1.NotFoundError('email', 'Failed to read email body.');
162
- const parsed = (0, json_1.parseObject)(raw);
163
- const { body, quoted, separator } = (0, mail_1.splitQuotedOriginal)(decodeBody(parsed.body));
164
- const attachmentNames = (0, json_1.toArray)(parsed.attachmentNames).map(json_1.str);
165
- // The quoted thread is context, never the sender's own answer, so it is capped
166
- // harder than the reply itself — its useful part (what was asked) is at the
167
- // top of it.
168
- const quotedCap = Math.min(maxChars, 4000);
132
+ body = ${BODY_B64_PS}
133
+ attachmentNames = @($attachmentNames)
134
+ })
135
+ `, 'quick');
136
+ const e = (0, json_1.record)(output);
169
137
  return {
170
- entryId: (0, json_1.str)(parsed.entryId),
171
- subject: (0, json_1.str)(parsed.subject),
172
- senderName: (0, json_1.str)(parsed.senderName),
173
- senderEmail: (0, json_1.str)(parsed.senderEmail),
174
- receivedTime: (0, json_1.str)(parsed.receivedTime),
175
- body: body.length > maxChars ? body.slice(0, maxChars) : body,
176
- truncated: body.length > maxChars,
177
- bodyLength: body.length,
178
- quoteSeparator: separator,
179
- quotedLength: quoted.length,
180
- quotedOriginal: includeQuoted ? quoted.slice(0, quotedCap) : '',
181
- attachmentNames,
182
- attachmentCount: attachmentNames.length,
138
+ entryId: (0, json_1.str)(e.entryId) || email.entryId,
139
+ subject: (0, json_1.str)(e.subject),
140
+ senderName: (0, json_1.str)(e.senderName),
141
+ senderEmail: (0, json_1.str)(e.senderEmail),
142
+ receivedTime: (0, json_1.str)(e.receivedTime),
143
+ body: (0, json_1.base64Text)(e.body),
144
+ attachmentNames: (0, json_1.toArray)(e.attachmentNames).map(json_1.str),
183
145
  };
184
146
  }
185
147
  /**
186
- * Walk every folder under the Inbox (recursively) for one account and return
187
- * the emails matching `filter` — full body included, attachments listed by
188
- * name but not saved.
148
+ * Walk every folder under the Inbox for the mail matching the request.
189
149
  *
190
- * Built for a scan that doesn't know in advance which subfolder holds what
191
- * it's after; `readInboxEmails` covers the cheaper "one known folder" case.
192
- *
193
- * `daysBack` bounds the scan to items received within that many days — what a
194
- * daily batch run wants, since a subject filter alone walks the whole Inbox
195
- * tree and returns every match ever received. 0 keeps that unbounded
196
- * behavior, and is only sane paired with `filter.subjectLike` so Restrict can
197
- * narrow the set server-side before anything crosses COM. Where the store
198
- * won't run that subject query — an IMAP mailbox accepts it and then matches
199
- * nothing — an unbounded scan falls back to the last 60 days rather than
200
- * walking every item the mailbox has ever held.
150
+ * The window is enforced twice — a date Restrict per folder, then a per-item
151
+ * check — because Restrict is not reliable across stores, and a scan that
152
+ * silently widens on some mailboxes is the failure this guards against.
153
+ * `subjectLike` also drives a server-side prefilter, purely as an optimization:
154
+ * every subject is re-checked, so a store that can't run the query (IMAP stores
155
+ * accept it and match nothing) returns the same set, only slower.
201
156
  */
202
- async function searchInboxByFilter(emailAccount, filter = {}, daysBack = 0) {
203
- if (process.platform !== 'win32')
204
- return [];
205
- const subjectLike = filter.subjectLike ? (0, run_1.psEscape)(filter.subjectLike) : '';
206
- // DASL's `like` takes SQL wildcards, so the glob the filter is written in is
207
- // translated for the server-side prefilter. The client-side re-check keeps
208
- // the glob form, which is what PowerShell's own -like reads.
209
- const subjectDasl = filter.subjectLike
210
- ? (0, run_1.psEscape)(filter.subjectLike.replace(/\*/g, '%').replace(/\?/g, '_'))
211
- : '';
212
- const subjectPatternSrc = filter.subjectPattern ? (0, run_1.psEscape)(filter.subjectPattern.source) : '';
213
- const script = `
214
- $subjLike = '${subjectLike}'
215
- $subjDasl = '${subjectDasl}'
216
- ${(0, scripts_1.accountScript)(emailAccount)}
217
- ${(0, scripts_1.namedStoreScript)(emailAccount)}
218
- $inbox = $store.GetDefaultFolder(6)
219
- $results = @()
220
- $seen = @{}
221
- $folders = [System.Collections.ArrayList]@($inbox)
222
- $fi = 0
223
- while ($fi -lt $folders.Count) {
224
- try { foreach ($sub in $folders[$fi].Folders) { [void]$folders.Add($sub) } } catch {}
225
- $fi++
157
+ async function searchInboxByFilter(request) {
158
+ const subjectFiltered = !!(request.subjectLike || request.subjectPattern || request.excludeReplies);
159
+ // The server-side prefilter speaks DASL, whose `like` wildcards are % and _.
160
+ // A literal % or _ in the glob is left as a wildcard there: that can only
161
+ // widen the prefilter, and the exact test below narrows it again.
162
+ const subjectDasl = request.subjectLike ? request.subjectLike.replace(/\*/g, '%').replace(/\?/g, '_') : '';
163
+ const output = await (0, run_1.runPowerShellJson)(`${(0, scripts_1.accountScript)(request.account)}
164
+ ${scripts_1.NAMED_STORE_PS}
165
+ $inbox = $store.GetDefaultFolder(${(0, run_1.psInt)(mail_1.FolderId.Inbox)})
166
+ $subjectLike = ${(0, run_1.psString)(request.subjectLike ? (0, mail_1.subjectGlobSource)(request.subjectLike) : '')}
167
+ $subjectDasl = ${(0, run_1.psString)(subjectDasl)}
168
+ $subjectPattern = ${(0, run_1.psString)(request.subjectPattern?.source ?? '')}
169
+ $replyPattern = ${(0, run_1.psString)(request.excludeReplies ? mail_1.REPLY_PREFIX_SOURCE : '')}
170
+ $requireSubject = ${(0, run_1.psBool)(subjectFiltered)}
171
+ $requireAttachment = ${(0, run_1.psBool)(request.requireAttachment)}
172
+ $includeBody = ${(0, run_1.psBool)(request.includeBody)}
173
+ ${(0, scripts_1.cutoffScript)(request.daysBack)}
174
+ # Sent, Drafts, Deleted and Junk are siblings of the Inbox on an Exchange profile
175
+ # and CHILDREN of it on an IMAP one. They are cut by id — their names are
176
+ # localized — before the walk, so each takes its whole subtree with it.
177
+ $skipIds = @{}
178
+ foreach ($role in @(${mail_1.NON_INCOMING_ROOTS.map(run_1.psInt).join(', ')})) {
179
+ try {
180
+ $roleFolder = $store.GetDefaultFolder($role)
181
+ if ($roleFolder -ne $null) { $skipIds[[string]$roleFolder.EntryID] = $true }
182
+ } catch {}
226
183
  }
227
- # The subject prefilter is a DASL property query rather than Outlook's Jet
228
- # syntax: "[Subject] like '...'" is rejected outright by some stores — an IMAP
229
- # mailbox answers "Condition is not valid" — while DASL is accepted wherever
230
- # Restrict is. The two syntaxes cannot be mixed in one filter string, so the
231
- # date clause stays Jet and the subject one is applied as a second Restrict on
232
- # top of it. The pattern is CONCATENATED into the query, never interpolated:
233
- # interpolating would let a caller's $(...) run, and would break the query
234
- # outright on an apostrophe.
235
- $subjQuery = '@SQL=' + [char]34 + 'urn:schemas:httpmail:subject' + [char]34 + ' like ' + [char]39 + $subjDasl + [char]39
236
- $daysBack = ${daysBack}
237
- foreach ($folder in $folders) {
238
- if ($daysBack -gt 0) {
239
- $cutoff = (Get-Date).AddDays(-$daysBack).ToString('MM/dd/yyyy HH:mm')
240
- $filtered = $folder.Items.Restrict("[ReceivedTime] >= '$cutoff'")
241
- } else {
242
- $filtered = $folder.Items
184
+ $skipNames = ${(0, run_1.psArray)(request.excludeFolders)}
185
+ $onlyNames = ${(0, run_1.psArray)(request.includeFolders)}
186
+ function Test-FolderNamed([string]$name, [string]$path, $list) {
187
+ foreach ($entry in $list) {
188
+ if ($name -ieq $entry -or $path -ieq $entry) { return $true }
243
189
  }
244
- ${subjectLike ? ` # Narrowing on the subject server-side is what keeps this cheap — a date-only
245
- # restrict hands back every item in the window for every folder in the tree,
246
- # which is thousands of COM round-trips on a busy mailbox. It is only an
247
- # optimization though: every subject is re-checked below, so a store that
248
- # cannot run the query returns the same answer, slower.
249
- $narrowed = $null
190
+ return $false
191
+ }
192
+ # Traversal and scanning are separate, and the two lists are not symmetric: an
193
+ # excluded folder is never queued, so its subtree goes with it, while an
194
+ # included one only marks a folder the walk reached anyway — the walk still
195
+ # passes through folders nobody named to reach a nested one somebody did.
196
+ $rootName = ''
197
+ try { $rootName = [string]$inbox.Name } catch {}
198
+ $rootPath = ''
199
+ try { $rootPath = [string]$inbox.FolderPath } catch {}
200
+ $folders = [System.Collections.ArrayList]@($inbox)
201
+ $inScope = [System.Collections.ArrayList]@(($onlyNames.Count -eq 0) -or (Test-FolderNamed $rootName $rootPath $onlyNames))
202
+ $walked = 0
203
+ while ($walked -lt $folders.Count) {
250
204
  try {
251
- $candidate = $filtered.Restrict($subjQuery)
252
- if ($candidate.Count -gt 0) { $narrowed = $candidate }
205
+ foreach ($sub in $folders[$walked].Folders) {
206
+ if ($skipIds.ContainsKey([string]$sub.EntryID)) { continue }
207
+ $subName = ''
208
+ try { $subName = [string]$sub.Name } catch {}
209
+ $subPath = ''
210
+ try { $subPath = [string]$sub.FolderPath } catch {}
211
+ if (Test-FolderNamed $subName $subPath $skipNames) { continue }
212
+ [void]$folders.Add($sub)
213
+ [void]$inScope.Add(($onlyNames.Count -eq 0) -or (Test-FolderNamed $subName $subPath $onlyNames))
214
+ }
253
215
  } catch {}
254
- if ($narrowed -ne $null) {
255
- $filtered = $narrowed
256
- } elseif ($daysBack -le 0) {
257
- # Nothing narrowed it and nothing bounds it, which would walk every item
258
- # the folder has ever held; cap the sweep at a window instead.
259
- $cutoff = (Get-Date).AddDays(-60).ToString('MM/dd/yyyy HH:mm')
260
- $filtered = $folder.Items.Restrict("[ReceivedTime] >= '$cutoff'")
216
+ $walked++
217
+ }
218
+ # The subject prefilter is DASL rather than Jet: some stores reject Jet's
219
+ # "[Subject] like" outright, while DASL is accepted wherever Restrict is. The
220
+ # two syntaxes can't share one filter, so it is a second Restrict on top of the
221
+ # date one. The pattern is concatenated into the query, never interpolated.
222
+ $subjectQuery = '@SQL=' + [char]34 + 'urn:schemas:httpmail:subject' + [char]34 + ' like ' + [char]39 + $subjectDasl.Replace("'", "''") + [char]39
223
+ # A list, not an array: += copies the whole array per row, and a wide scan returns thousands.
224
+ $rows = New-Object System.Collections.ArrayList
225
+ $seen = @{}
226
+ for ($fx = 0; $fx -lt $folders.Count; $fx++) {
227
+ if (-not $inScope[$fx]) { continue }
228
+ $folder = $folders[$fx]
229
+ ${(0, scripts_1.itemsSinceScript)('folder', 'ReceivedTime', 'filtered').trim()}
230
+ if ($subjectDasl) {
231
+ try {
232
+ $candidate = $filtered.Restrict($subjectQuery)
233
+ if ($candidate.Count -gt 0) { $filtered = $candidate }
234
+ } catch {}
261
235
  }
262
- ` : ''} $fCount = $filtered.Count
263
- for ($i = 1; $i -le $fCount; $i++) {
264
- $item = $filtered.Item($i)
265
- if ($seen.ContainsKey($item.EntryID)) { continue }
266
- $seen[$item.EntryID] = $true
267
- $subject = $item.Subject
268
- if (-not $subject) { continue }
269
- $subject = $subject.Trim()
270
- ${subjectLike ? `if ($subject -notlike $subjLike) { continue }` : ''}
271
- ${subjectPatternSrc ? `if ($subject -notmatch '${subjectPatternSrc}') { continue }` : ''}
272
- ${filter.excludeReplies ? `if ($subject -imatch '^(re|fw[d]?)\\s*:') { continue }` : ''}
273
- ${filter.requireAttachment ? `if ($item.Attachments.Count -eq 0) { continue }` : ''}
274
- $attNames = @()
275
- foreach ($att in $item.Attachments) { $attNames += $att.FileName }
276
- $bodyRaw = if ($item.Body) { $item.Body } else { '' }
277
- $bodyB64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($bodyRaw))
236
+ $folderPath = ''
237
+ try { $folderPath = [string]$folder.FolderPath } catch {}
238
+ $count = $filtered.Count
239
+ for ($i = 1; $i -le $count; $i++) {
240
+ $item = $null
241
+ try { $item = $filtered.Item($i) } catch {}
242
+ # A restricted collection can hand back nothing for an index, and an item
243
+ # with no id can't be acted on by anything this package does.
244
+ if ($item -eq $null) { continue }
245
+ $entryId = [string]$item.EntryID
246
+ if (-not $entryId -or $seen.ContainsKey($entryId)) { continue }
247
+ $seen[$entryId] = $true
248
+ $subject = ''
249
+ try { $subject = ([string]$item.Subject).Trim() } catch {}
250
+ if ($requireSubject -and -not $subject) { continue }
251
+ if ($subjectLike -and $subject -notmatch $subjectLike) { continue }
252
+ if ($subjectPattern -and $subject -notmatch $subjectPattern) { continue }
253
+ if ($replyPattern -and $subject -imatch $replyPattern) { continue }
254
+ $received = $null
255
+ try { $received = $item.ReceivedTime } catch {}
256
+ if ($received -ne $null -and $received -lt $cutoff) { continue }
257
+ $attachmentNames = @()
258
+ try { foreach ($att in $item.Attachments) { $attachmentNames += [string]$att.FileName } } catch {}
259
+ if ($requireAttachment -and $attachmentNames.Count -eq 0) { continue }
260
+ $bodyRaw = ''
261
+ if ($includeBody) { try { $bodyRaw = [string]$item.Body } catch {} }
278
262
  ${scripts_1.SENDER_SMTP_PS}
279
- $received = ''
280
- try { $received = $item.ReceivedTime.ToString('yyyy-MM-dd HH:mm') } catch {}
281
- $results += [PSCustomObject]@{
282
- entryId = $item.EntryID
283
- storeId = $storeId
284
- subject = $item.Subject.Trim()
285
- senderName = if ($item.SenderName) { $item.SenderName } else { '' }
286
- senderEmail = $senderSmtp
287
- receivedTime = $received
288
- body = $bodyB64
289
- attachmentNames = $attNames
290
- folderPath = $folder.FolderPath
291
- }
263
+ [void]$rows.Add([PSCustomObject]@{
264
+ entryId = $entryId
265
+ storeId = $storeId
266
+ subject = $subject
267
+ senderName = [string]$item.SenderName
268
+ senderEmail = $senderSmtp
269
+ receivedTime = if ($received -ne $null) { $received.ToString('yyyy-MM-dd HH:mm') } else { '' }
270
+ body = ${BODY_B64_PS}
271
+ attachmentNames = @($attachmentNames)
272
+ folderPath = $folderPath
273
+ })
292
274
  }
293
275
  }
294
- ConvertTo-Json $results -Depth 3
295
- `;
296
- // Returns whole message bodies, so this is the call most likely to push against
297
- // the stdout cap — raise `maxBufferBytes` via configure() before a scan that
298
- // matches thousands of emails.
299
- return (0, json_1.parseArray)(await (0, run_1.runPowerShell)(script, 300000)).map(item => {
300
- const e = (0, json_1.record)(item);
276
+ ConvertTo-Json -Depth 4 -InputObject @($rows)
277
+ `, 'scan');
278
+ return (0, json_1.toArray)(output).map(row => {
279
+ const e = (0, json_1.record)(row);
301
280
  return {
302
281
  entryId: (0, json_1.str)(e.entryId),
303
282
  storeId: (0, json_1.str)(e.storeId),
@@ -305,64 +284,54 @@ ConvertTo-Json $results -Depth 3
305
284
  senderName: (0, json_1.str)(e.senderName),
306
285
  senderEmail: (0, json_1.str)(e.senderEmail),
307
286
  receivedTime: (0, json_1.str)(e.receivedTime),
308
- body: decodeBody(e.body),
287
+ body: (0, json_1.base64Text)(e.body),
309
288
  attachmentNames: (0, json_1.toArray)(e.attachmentNames).map(json_1.str),
310
289
  folderPath: (0, json_1.str)(e.folderPath),
311
290
  };
312
291
  });
313
292
  }
314
- /**
315
- * Read the email currently selected (or open) in Outlook — full body,
316
- * attachments listed by name but not saved.
317
- */
293
+ /** The email currently selected — or open — in Outlook. */
318
294
  async function readSelectedEmail() {
319
- (0, run_1.requireWindows)();
320
- const script = `
295
+ const output = await (0, run_1.runPowerShellJson)(`
321
296
  $outlook = New-Object -ComObject Outlook.Application
322
297
  $item = $null
323
298
  try {
324
299
  $explorer = $outlook.ActiveExplorer()
325
- if ($explorer -ne $null) {
326
- $sel = $explorer.Selection
327
- if ($sel -ne $null -and $sel.Count -ge 1) { $item = $sel.Item(1) }
328
- }
300
+ if ($explorer -ne $null -and $explorer.Selection.Count -ge 1) { $item = $explorer.Selection.Item(1) }
329
301
  } catch {}
330
302
  if ($item -eq $null) {
331
303
  try {
332
- $insp = $outlook.ActiveInspector()
333
- if ($insp -ne $null) { $item = $insp.CurrentItem }
304
+ $inspector = $outlook.ActiveInspector()
305
+ if ($inspector -ne $null) { $item = $inspector.CurrentItem }
334
306
  } catch {}
335
307
  }
336
- if ($item -eq $null) { throw 'No email is selected in Outlook. Open Outlook, select (or open) an email, then try again.' }
337
- if ($item.MessageClass -notlike 'IPM.Note*') { throw 'The selected Outlook item is not an email.' }
338
- $attNames = @()
339
- foreach ($att in $item.Attachments) { $attNames += $att.FileName }
340
- $subject = ''
341
- if ($item.Subject) { $subject = $item.Subject.Trim() }
342
- $bodyRaw = if ($item.Body) { $item.Body } else { '' }
343
- $bodyB64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($bodyRaw))
308
+ if ($item -eq $null) {
309
+ throw "${(0, errors_1.failureTag)('NOT_FOUND', 'email')}No email is selected in Outlook. Select or open an email, then try again."
310
+ }
311
+ if ([string]$item.MessageClass -notlike 'IPM.Note*') {
312
+ throw "${(0, errors_1.failureTag)('INVALID_REQUEST')}The item selected in Outlook is not an email."
313
+ }
314
+ $attachmentNames = @()
315
+ try { foreach ($att in $item.Attachments) { $attachmentNames += [string]$att.FileName } } catch {}
316
+ $bodyRaw = ''
317
+ try { $bodyRaw = [string]$item.Body } catch {}
344
318
  $storeId = ''
345
- try { $storeId = $item.Parent.Store.StoreID } catch {}
319
+ try { $storeId = [string]$item.Parent.Store.StoreID } catch {}
346
320
  ${scripts_1.SENDER_SMTP_PS}
347
321
  $received = ''
348
322
  try { $received = $item.ReceivedTime.ToString('yyyy-MM-dd HH:mm') } catch {}
349
- $result = [PSCustomObject]@{
350
- entryId = $item.EntryID
351
- storeId = $storeId
352
- subject = $subject
353
- senderName = if ($item.SenderName) { $item.SenderName } else { '' }
354
- senderEmail = $senderSmtp
355
- receivedTime = $received
356
- body = $bodyB64
357
- attachmentNames = $attNames
358
- }
359
- ConvertTo-Json $result -Depth 3
360
- `;
361
- const raw = await (0, run_1.runPowerShell)(script, 30000);
362
- if (!raw || raw.trim() === '' || raw.trim() === 'null') {
363
- throw new errors_1.NotFoundError('email', 'Failed to read the selected Outlook email.');
364
- }
365
- const e = (0, json_1.parseObject)(raw);
323
+ ConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{
324
+ entryId = [string]$item.EntryID
325
+ storeId = $storeId
326
+ subject = ([string]$item.Subject).Trim()
327
+ senderName = [string]$item.SenderName
328
+ senderEmail = $senderSmtp
329
+ receivedTime = $received
330
+ body = ${BODY_B64_PS}
331
+ attachmentNames = @($attachmentNames)
332
+ })
333
+ `, 'quick');
334
+ const e = (0, json_1.record)(output);
366
335
  return {
367
336
  entryId: (0, json_1.str)(e.entryId),
368
337
  storeId: (0, json_1.str)(e.storeId),
@@ -370,35 +339,21 @@ ConvertTo-Json $result -Depth 3
370
339
  senderName: (0, json_1.str)(e.senderName),
371
340
  senderEmail: (0, json_1.str)(e.senderEmail),
372
341
  receivedTime: (0, json_1.str)(e.receivedTime),
373
- body: decodeBody(e.body),
374
- attachmentNames: (0, json_1.toArray)(e.attachmentNames).map(json_1.str),
342
+ body: (0, json_1.base64Text)(e.body),
343
+ attachmentNames: (0, json_1.strList)(e.attachmentNames),
375
344
  };
376
345
  }
377
346
  /**
378
- * Open an email in Outlook by its EntryID.
379
- *
380
- * `storeId` disambiguates across mailboxes — without one, `GetItemFromID` only
381
- * looks in the default store, so an id from a shared or secondary mailbox
382
- * simply isn't found. It's tried first and the bare lookup is the fallback, so
383
- * a caller holding a stale StoreID (or none) still resolves whatever it can.
347
+ * Open an email in Outlook.
384
348
  *
385
- * EntryIDs are rewritten when an item MOVES between folders, so an id recorded
386
- * before its mail was filed can resolve to nothing — or, worse, to a different
387
- * message. The caller is the one that knows what the email was supposed to be;
388
- * this throws a plain not-found rather than guessing.
349
+ * Entry ids are rewritten when an item MOVES between folders, so an id recorded
350
+ * before its mail was filed can stop resolving. That fails as NOT_FOUND rather
351
+ * than guessing — the caller is the one who knows what the email was.
389
352
  */
390
- async function openOutlookEmail(entryId, storeId) {
391
- (0, run_1.requireWindows)();
392
- const script = `
393
- $outlook = New-Object -ComObject Outlook.Application
394
- $ns = $outlook.GetNamespace('mapi')
395
- $ns.Logon()
396
- $item = $null
397
- ${storeId ? `try { ${(0, scripts_1.getItemScript)(entryId, storeId)} } catch { $item = $null }` : ''}
398
- if ($item -eq $null) { try { ${(0, scripts_1.getItemScript)(entryId)} } catch { $item = $null } }
399
- if ($item -eq $null) { throw "Email not found for EntryID '${(0, run_1.psEscape)(entryId)}'" }
353
+ async function openOutlookEmail(email) {
354
+ await (0, run_1.runPowerShell)(`${scripts_1.SESSION_PS}
355
+ ${(0, scripts_1.itemLookupScript)(email)}
400
356
  $item.Display()
401
- `;
402
- await (0, run_1.runPowerShell)(script);
357
+ `, 'quick');
403
358
  }
404
359
  //# sourceMappingURL=read.js.map