@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
package/dist/mac/read.js CHANGED
@@ -8,49 +8,32 @@ exports.openOutlookEmail = openOutlookEmail;
8
8
  // Reading mail on macOS: one folder, one item, a whole Inbox tree, or whatever
9
9
  // is selected in the running Outlook.
10
10
  //
11
- // Every reader here is two passes. Pass 1 indexes a folder with bulk property
12
- // reads (one Apple event each); pass 2 fetches the expensive fields — body,
11
+ // Every reader is two passes. Pass 1 indexes a folder with bulk property reads
12
+ // (one Apple event each); pass 2 fetches the expensive fields — body,
13
13
  // attachments, sender — only for the messages that survived. Reading
14
14
  // per-message in a loop costs an event per property and is unusably slow on a
15
15
  // real mailbox, which is the whole reason for the shape.
16
16
  const run_1 = require("./run");
17
+ const profile_1 = require("./profile");
17
18
  const scripts_1 = require("./scripts");
18
- const mail_1 = require("../mail");
19
19
  const errors_1 = require("../errors");
20
- /** How much of a body readInboxEmails previews, matching the Windows reader. */
21
- const PREVIEW_CHARS = 600;
20
+ const mail_1 = require("../mail");
22
21
  /**
23
- * Read recent messages for the given account, newest first.
24
- *
25
- * `entryId` here is Outlook for Mac's small integer message id (e.g. "779"), not
26
- * the MAPI EntryID string Windows returns. The two are not interchangeable, so
27
- * don't persist one and look it up on the other platform.
22
+ * Recent mail from the Inbox root or one folder, newest first. Entry ids here
23
+ * are Outlook for Mac's integer message ids, not Windows EntryIDs.
28
24
  */
29
- async function readInboxEmails(emailAccount, daysBack = 60, limit = 50, folder) {
30
- const days = Math.max(0, Math.floor(daysBack));
31
- const cap = Math.max(0, Math.floor(limit));
32
- if (cap === 0)
33
- return [];
34
- const ref = folder ? (0, mail_1.mailFolderRef)(folder) : { rootId: 6, rootLabel: 'Inbox', segments: [] };
35
- // A folder argument that trims away to nothing ("\\", " ") would otherwise
36
- // read the Inbox root and look like it had scoped — the exact silent
37
- // mis-scoping this parameter exists to prevent. Mirrors the Windows check.
38
- if (folder && folder.trim() && ref.segments.length === 0 && ref.rootId === 6
39
- && folder.trim().toLowerCase() !== 'inbox') {
40
- throw new errors_1.NotFoundError('folder', `Folder '${folder}' does not name a folder under the Inbox.`);
41
- }
42
- const resolveScope = (0, scripts_1.mailScopeSnippet)(ref, folder || '');
43
- const folderPath = (0, scripts_1.macFolderPath)(emailAccount, ref.rootLabel, ref.segments);
44
- const isOutgoing = (0, mail_1.isOutgoingRoot)(ref.rootId);
45
- // Pass 1 — index the folder. Folder order isn't documented, so sort here
46
- // rather than trusting Outlook to hand back newest-first.
47
- const indexScript = `tell application "Microsoft Outlook"
48
- ${(0, scripts_1.accountLookupSnippet)(emailAccount)}
49
- ${resolveScope}
50
- set cutoff to (current date) - (${days} * days)
25
+ async function readInboxEmails(request) {
26
+ const acct = await (0, scripts_1.resolveMacAccount)(request.account);
27
+ const folderPath = (0, scripts_1.macFolderPath)(request.account, request.folder.rootLabel, request.folder.segments);
28
+ const outgoing = (0, mail_1.isOutgoingRoot)(request.folder.rootId);
29
+ // Pass 1 — index the folder. Its order isn't documented, so it is sorted here.
30
+ const index = (await (0, run_1.runOsaScript)(`tell application "Microsoft Outlook"
31
+ ${(0, scripts_1.accountLookupSnippet)(acct)}
32
+ ${(0, scripts_1.mailScopeSnippet)(acct, request.folder, request.folderLabel)}
33
+ set cutoff to (current date) - (${(0, run_1.asInt)(request.daysBack)} * days)
51
34
  set idList to id of every message of scopeFolder
52
35
  try
53
- set timeList to ${(0, scripts_1.dateProperty)(ref.rootId)} of every message of scopeFolder
36
+ set timeList to ${(0, scripts_1.dateProperty)(request.folder.rootId)} of every message of scopeFolder
54
37
  on error
55
38
  set timeList to {}
56
39
  end try
@@ -59,16 +42,14 @@ ${resolveScope}
59
42
  repeat with i from 1 to (count of idList)
60
43
  set d to missing value
61
44
  if hasTimes then set d to item i of timeList
62
- -- An item carrying no timestamp of its own (an unsent draft filed into a
63
- -- mail folder) is skipped rather than crashing the walk, matching what the
64
- -- Windows Restrict on the same property does with it.
45
+ -- An item with no timestamp of its own (an unsent draft filed into a
46
+ -- mail folder) is skipped, as the Windows date filter skips it.
65
47
  if d is not missing value and d is greater than or equal to cutoff then
66
48
  set out to out & (item i of idList as string) & tab & my isoDate(d) & linefeed
67
49
  end if
68
50
  end repeat
69
51
  return out
70
- end tell`;
71
- const index = (await (0, run_1.runOsaScript)(indexScript, 60000))
52
+ end tell`, 'standard'))
72
53
  .split('\n')
73
54
  .map(line => line.trim())
74
55
  .filter(Boolean)
@@ -76,59 +57,54 @@ end tell`;
76
57
  const [id, receivedTime] = line.split('\t');
77
58
  return { id, receivedTime: receivedTime || '' };
78
59
  });
79
- // 'yyyy-MM-dd HH:mm' is lexicographically ordered, so plain string compare sorts it.
60
+ // 'yyyy-MM-dd HH:mm' sorts correctly as a string.
80
61
  index.sort((a, b) => b.receivedTime.localeCompare(a.receivedTime));
81
- const chosen = index.slice(0, cap);
62
+ const chosen = index.slice(0, request.limit);
82
63
  if (chosen.length === 0)
83
64
  return [];
84
- // Pass 2 — the expensive reads, only for messages we keep.
85
- const detailScript = `tell application "Microsoft Outlook"
86
- set wanted to {${chosen.map(c => c.id).join(', ')}}
65
+ // Pass 2 — the expensive reads, only for the messages kept.
66
+ const preview = request.previewChars > 0
67
+ ? ` set bodyText to ""
68
+ try
69
+ set bodyText to (plain text content of theMsg) as string
70
+ end try
71
+ if (length of bodyText) > ${(0, run_1.asInt)(request.previewChars)} then set bodyText to text 1 thru ${(0, run_1.asInt)(request.previewChars)} of bodyText`
72
+ : ' set bodyText to ""';
73
+ const raw = await (0, run_1.runOsaScript)(`tell application "Microsoft Outlook"
87
74
  set out to ""
88
- repeat with k from 1 to (count of wanted)
75
+ repeat with theId in ${(0, run_1.asIdList)(chosen.map(c => c.id))}
89
76
  set theMsg to missing value
90
77
  try
91
- set theMsg to message id (item k of wanted)
78
+ set theMsg to message id theId
92
79
  end try
93
80
  if theMsg is not missing value then
94
81
  set subj to ""
95
82
  try
96
83
  set subj to (subject of theMsg) as string
97
84
  end try
98
- ${isOutgoing ? (0, scripts_1.firstRecipientSnippet)('theMsg', ' ') : (0, scripts_1.senderSnippet)('theMsg', ' ')}
99
- set bodyText to ""
100
- try
101
- set bodyText to (plain text content of theMsg) as string
102
- end try
103
- if (length of bodyText) > ${PREVIEW_CHARS} then set bodyText to text 1 thru ${PREVIEW_CHARS} of bodyText
85
+ ${outgoing ? (0, scripts_1.firstRecipientSnippet)(' ') : (0, scripts_1.senderSnippet)(' ')}
86
+ ${preview}
104
87
  set attNames to {}
105
88
  try
106
89
  set attNames to name of every attachment of theMsg
107
90
  end try
108
- set out to out & ${(0, run_1.asRow)([
109
- '(id of theMsg as string)',
110
- 'subj',
111
- 'sndName',
112
- 'sndAddr',
113
- 'bodyText',
114
- 'my sanitizeList(attNames)',
115
- ])}
91
+ set out to out & ${(0, run_1.asRow)(['(id of theMsg as string)', 'subj', 'sndName', 'sndAddr', 'bodyText', 'my sanitizePositionalList(attNames)'])}
116
92
  end if
117
93
  end repeat
118
94
  return out
119
- end tell`;
120
- const byId = new Map(chosen.map(c => [c.id, c.receivedTime]));
121
- return (0, run_1.splitRecords)(await (0, run_1.runOsaScript)(detailScript, 120000)).map(record => {
95
+ end tell`, 'standard');
96
+ const receivedById = new Map(chosen.map(c => [c.id, c.receivedTime]));
97
+ return (0, run_1.splitRecords)(raw).map(record => {
122
98
  const parts = (0, run_1.splitFields)(record);
123
99
  const id = (0, run_1.field)(parts, 0);
124
- const attachmentNames = (0, run_1.splitList)((0, run_1.field)(parts, 5));
100
+ const attachmentNames = (0, run_1.splitPositionalList)((0, run_1.field)(parts, 5));
125
101
  return {
126
102
  entryId: id,
127
- storeId: '', // macOS AppleScript has no StoreID equivalent
103
+ storeId: '',
128
104
  subject: (0, run_1.field)(parts, 1).trim(),
129
105
  senderName: (0, run_1.field)(parts, 2),
130
106
  senderEmail: (0, run_1.field)(parts, 3),
131
- receivedTime: byId.get(id) || '',
107
+ receivedTime: receivedById.get(id) || '',
132
108
  bodyPreview: (0, run_1.field)(parts, 4),
133
109
  attachmentNames,
134
110
  attachmentCount: attachmentNames.length,
@@ -137,216 +113,198 @@ end tell`;
137
113
  });
138
114
  }
139
115
  /**
140
- * Read one email's full plain-text body by message id.
141
- *
142
- * `storeId` is accepted for signature parity and ignored: macOS has no StoreID,
143
- * and `message id N` resolves against the application rather than one folder, so
144
- * the message is found wherever it currently sits — including a subfolder.
145
- *
146
- * Uses `plain text content`, matching the Windows reader's use of `.Body`: the
147
- * same tradeoff applies, so an HTML table's rows flatten and tabular figures are
148
- * better read from an attachment.
116
+ * One email's full plain-text body. `storeId` has no macOS meaning: `message id
117
+ * N` resolves against the application, wherever the message sits.
149
118
  */
150
- async function readEmailBody(entryId, _storeId, maxChars = 8000, includeQuoted = false) {
151
- const id = (0, scripts_1.macMessageId)(entryId);
152
- const script = `tell application "Microsoft Outlook"
119
+ async function readEmailBody(email) {
120
+ const id = (0, scripts_1.macMessageId)(email.entryId);
121
+ const parts = (0, run_1.summaryFields)(await (0, run_1.runOsaScript)(`tell application "Microsoft Outlook"
153
122
  ${(0, scripts_1.messageLookupSnippet)(id)}
154
123
  ${(0, scripts_1.messageDetailSnippet)(true)}
155
124
  return ${(0, run_1.asRow)((0, scripts_1.messageDetailFields)(true))}
156
- end tell`;
157
- // The body is emitted LAST so a stray separator in earlier fields can't shift it.
158
- const parts = (0, run_1.summaryFields)(await (0, run_1.runOsaScript)(script, 60000));
159
- const attachmentNames = (0, run_1.splitList)((0, run_1.field)(parts, scripts_1.MessageDetail.attachmentNames));
160
- const { body, quoted, separator } = (0, mail_1.splitQuotedOriginal)((0, run_1.field)(parts, scripts_1.MessageDetail.body));
161
- // The quoted thread is context, never the priced content, so it is capped
162
- // harder than the reply itself — matching the Windows reader.
163
- const quotedCap = Math.min(maxChars, 4000);
125
+ end tell`, 'quick'));
164
126
  return {
165
127
  entryId: (0, run_1.field)(parts, scripts_1.MessageDetail.id) || id,
166
128
  subject: (0, run_1.field)(parts, scripts_1.MessageDetail.subject).trim(),
167
129
  senderName: (0, run_1.field)(parts, scripts_1.MessageDetail.senderName),
168
130
  senderEmail: (0, run_1.field)(parts, scripts_1.MessageDetail.senderEmail),
169
131
  receivedTime: (0, run_1.field)(parts, scripts_1.MessageDetail.receivedTime),
170
- body: body.length > maxChars ? body.slice(0, maxChars) : body,
171
- truncated: body.length > maxChars,
172
- bodyLength: body.length,
173
- quoteSeparator: separator,
174
- quotedLength: quoted.length,
175
- quotedOriginal: includeQuoted ? quoted.slice(0, quotedCap) : '',
176
- attachmentNames,
177
- attachmentCount: attachmentNames.length,
132
+ body: (0, run_1.field)(parts, scripts_1.MessageDetail.body),
133
+ attachmentNames: (0, run_1.splitPositionalList)((0, run_1.field)(parts, scripts_1.MessageDetail.attachmentNames)),
178
134
  };
179
135
  }
180
136
  /**
181
- * Translate a Restrict-style `like` pattern to a regex.
182
- *
183
- * Windows hands `subjectLike` to Outlook's server-side Restrict; AppleScript has
184
- * no equivalent, so the same pattern is applied here instead. `%` and `*` are
185
- * the any-run wildcards, `_` and `?` match one character, and the match is
186
- * anchored because that is what SQL `like` means.
187
- */
188
- function likePattern(pattern) {
189
- let source = '';
190
- for (const ch of pattern) {
191
- if (ch === '%' || ch === '*')
192
- source += '.*';
193
- else if (ch === '_' || ch === '?')
194
- source += '.';
195
- else
196
- source += ch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
197
- }
198
- return new RegExp(`^${source}$`, 'i');
199
- }
200
- /**
201
- * Walk every folder under the Inbox (recursively) for one account and return the
202
- * emails matching `filter` — full body included, attachments listed by name but
203
- * not saved.
137
+ * Walk every folder under the Inbox for the mail matching the request.
204
138
  *
205
- * The filtering happens here rather than in Outlook: `Items.Restrict` is a COM
206
- * facility with no AppleScript counterpart, so the folder walk indexes each
207
- * folder in two bulk reads and the subject/date tests run on the index. The
208
- * observable contract is the same; the cost profile is not, and a `daysBack` of
209
- * 0 over a large mailbox tree is correspondingly heavier here.
210
- *
211
- * `subjectPattern` is applied case-insensitively regardless of the RegExp's own
212
- * flags, matching PowerShell's `-match`.
139
+ * The filtering happens here rather than in Outlook: there is no AppleScript
140
+ * counterpart to COM's server-side `Items.Restrict`, so each folder is indexed in
141
+ * bulk reads and the subject and date tests run on the index. The results match
142
+ * Windows; the cost doesn't, and a wide window over a big tree is heavier here.
213
143
  */
214
- async function searchInboxByFilter(emailAccount, filter = {}, daysBack = 0) {
215
- const days = Math.max(0, Math.floor(daysBack));
144
+ async function searchInboxByFilter(request) {
145
+ const acct = await (0, scripts_1.resolveMacAccount)(request.account);
146
+ // AppleScript reaches a child folder by name, so a path entry narrows to its leaf.
147
+ const asNameList = (list) => `{${list.map(mail_1.folderLeafName).filter(Boolean).map(run_1.asString).join(', ')}}`;
148
+ const rootInScope = request.includeFolders.length === 0 ? 'true' : 'false';
149
+ // Sent, Drafts, Deleted and Junk hang UNDER the Inbox on an IMAP profile, so
150
+ // their ids are collected first and the walk never descends into them — by
151
+ // the profile's own id where it records one, through the account otherwise.
152
+ const skipIds = ['sent items', 'deleted items', 'drafts', 'junk mail']
153
+ .map(term => {
154
+ const id = acct.folderIds?.[term];
155
+ return id !== undefined
156
+ ? ` set end of skipIds to ${id}`
157
+ : ` try
158
+ set end of skipIds to (id of (${term} of targetAcct))
159
+ end try`;
160
+ })
161
+ .join('\n');
216
162
  // Pass 1 — index every folder under the Inbox: path, id, subject, received.
217
- const indexScript = `on scanFolder(theFolder, prefix, cutoff, useCutoff)
163
+ const indexed = (0, run_1.splitRecords)(await (0, run_1.runOsaScript)(`on scanFolder(theFolder, prefix, cutoff, skipIds, skipNames, onlyNames, inScope)
218
164
  set out to ""
165
+ set idList to {}
166
+ set subjList to {}
167
+ set timeList to {}
168
+ -- A folder the walk only passes through is never read: the bulk reads are
169
+ -- the expensive part.
170
+ if inScope then
171
+ tell application "Microsoft Outlook"
172
+ set idList to id of every message of theFolder
173
+ set subjList to subject of every message of theFolder
174
+ try
175
+ set timeList to time received of every message of theFolder
176
+ on error
177
+ set timeList to {}
178
+ end try
179
+ end tell
180
+ end if
219
181
  tell application "Microsoft Outlook"
220
- set idList to id of every message of theFolder
221
- set subjList to subject of every message of theFolder
222
- try
223
- set timeList to time received of every message of theFolder
224
- on error
225
- set timeList to {}
226
- end try
227
182
  set subs to mail folders of theFolder
228
183
  end tell
229
184
  set hasTimes to ((count of timeList) is (count of idList))
230
185
  repeat with i from 1 to (count of idList)
231
186
  set d to missing value
232
187
  if hasTimes then set d to item i of timeList
233
- set keep to true
234
- if d is missing value then
235
- -- A folder whose items carry no readable receive time can only be
236
- -- included when the caller isn't bounding by date; silently dropping
237
- -- it would look exactly like an empty folder.
238
- set keep to not useCutoff
239
- else if useCutoff and d is less than cutoff then
240
- set keep to false
241
- end if
242
- if keep then
243
- set out to out & ${(0, run_1.asRow)([
244
- 'prefix',
245
- '(item i of idList as string)',
246
- '(item i of subjList)',
247
- 'my isoDate(d)',
248
- ])}
188
+ -- An item with no readable receive time is kept whatever the window
189
+ -- says; dropping it would make a folder of them look empty.
190
+ if d is missing value or d is not less than cutoff then
191
+ set out to out & ${(0, run_1.asRow)(['prefix', '(item i of idList as string)', '(item i of subjList)', 'my isoDate(d)'])}
249
192
  end if
250
193
  end repeat
251
194
  repeat with f in subs
252
- set childName to ""
195
+ set skipThis to false
196
+ try
197
+ if skipIds contains (id of f) then set skipThis to true
198
+ end try
253
199
  try
254
- set childName to (name of f) as string
200
+ if skipNames contains ((name of f) as string) then set skipThis to true
255
201
  end try
256
- set out to out & my scanFolder(f, prefix & ${run_1.AS_LIST_SEP} & childName, cutoff, useCutoff)
202
+ if not skipThis then
203
+ set childName to ""
204
+ try
205
+ set childName to (name of f) as string
206
+ end try
207
+ set childScope to (count of onlyNames) is 0
208
+ try
209
+ if onlyNames contains childName then set childScope to true
210
+ end try
211
+ set out to out & my scanFolder(f, prefix & ${run_1.AS_LIST_SEP} & childName, cutoff, skipIds, skipNames, onlyNames, childScope)
212
+ end if
257
213
  end repeat
258
214
  return out
259
215
  end scanFolder
260
216
 
261
217
  tell application "Microsoft Outlook"
262
- ${(0, scripts_1.accountLookupSnippet)(emailAccount)}
263
- set rootInbox to inbox of targetAcct
218
+ ${(0, scripts_1.accountLookupSnippet)(acct)}
219
+ ${(0, scripts_1.rootFolderSnippet)(acct, 'inbox', 'rootInbox')}
264
220
  set rootName to (name of rootInbox) as string
221
+ set skipIds to {}
222
+ ${skipIds}
223
+ set skipNames to ${asNameList(request.excludeFolders)}
224
+ set onlyNames to ${asNameList(request.includeFolders)}
265
225
  end tell
266
- set cutoff to (current date) - (${days} * days)
267
- return my scanFolder(rootInbox, rootName, cutoff, ${days > 0 ? 'true' : 'false'})`;
268
- const likeRe = filter.subjectLike ? likePattern(filter.subjectLike) : null;
269
- // PowerShell's -match is case-insensitive by default, so the pattern is
270
- // rebuilt with `i` regardless of the flags the caller's RegExp carries.
271
- const patternRe = filter.subjectPattern
272
- ? new RegExp(filter.subjectPattern.source, 'i')
226
+ set cutoff to (current date) - (${(0, run_1.asInt)(request.daysBack)} * days)
227
+ return my scanFolder(rootInbox, rootName, cutoff, skipIds, skipNames, onlyNames, ${rootInScope})`, 'scan'));
228
+ const subjectFiltered = !!(request.subjectLike || request.subjectPattern || request.excludeReplies);
229
+ const likeRe = request.subjectLike ? new RegExp((0, mail_1.subjectGlobSource)(request.subjectLike), 'i') : null;
230
+ // PowerShell's -match is case-insensitive, so this test is too, whatever the
231
+ // caller's flags. Only u and v are kept: they decide how the source parses,
232
+ // and without u, /\p{L}/u would match the literal text "p{L}". Keeping g or y
233
+ // would carry lastIndex from one subject to the next.
234
+ const patternRe = request.subjectPattern
235
+ ? new RegExp(request.subjectPattern.source, 'i' + request.subjectPattern.flags.replace(/[^uv]/g, ''))
273
236
  : null;
274
- const candidates = (0, run_1.splitRecords)(await (0, run_1.runOsaScript)(indexScript, 300000))
237
+ const candidates = indexed
275
238
  .map(record => {
276
239
  const parts = (0, run_1.splitFields)(record);
277
240
  const segments = (0, run_1.splitList)((0, run_1.field)(parts, 0));
278
241
  return {
279
- folderPath: (0, scripts_1.macFolderPath)(emailAccount, segments[0] || 'Inbox', segments.slice(1)),
242
+ folderPath: (0, scripts_1.macFolderPath)(request.account, segments[0] || 'Inbox', segments.slice(1)),
280
243
  id: (0, run_1.field)(parts, 1),
281
244
  subject: (0, run_1.field)(parts, 2).trim(),
282
245
  receivedTime: (0, run_1.field)(parts, 3),
283
246
  };
284
247
  })
285
248
  .filter(candidate => {
286
- if (!candidate.subject)
249
+ if (subjectFiltered && !candidate.subject)
287
250
  return false;
288
251
  if (likeRe && !likeRe.test(candidate.subject))
289
252
  return false;
290
253
  if (patternRe && !patternRe.test(candidate.subject))
291
254
  return false;
292
- if (filter.excludeReplies && /^(re|fwd?)\s*:/i.test(candidate.subject))
255
+ if (request.excludeReplies && mail_1.REPLY_PREFIX.test(candidate.subject))
293
256
  return false;
294
257
  return true;
295
258
  });
296
259
  if (candidates.length === 0)
297
260
  return [];
298
- // Pass 2 — bodies, senders and attachment names for the survivors only.
299
- const detailScript = `tell application "Microsoft Outlook"
300
- set wanted to {${candidates.map(c => c.id).join(', ')}}
261
+ // Pass 2 — senders, attachment names and (when asked) bodies, for the survivors only.
262
+ const body = request.includeBody
263
+ ? ` set bodyText to ""
264
+ try
265
+ set bodyText to (plain text content of theMsg) as string
266
+ end try`
267
+ : ' set bodyText to ""';
268
+ const raw = await (0, run_1.runOsaScript)(`tell application "Microsoft Outlook"
301
269
  set out to ""
302
- repeat with k from 1 to (count of wanted)
270
+ repeat with theId in ${(0, run_1.asIdList)(candidates.map(c => c.id))}
303
271
  set theMsg to missing value
304
272
  try
305
- set theMsg to message id (item k of wanted)
273
+ set theMsg to message id theId
306
274
  end try
307
275
  if theMsg is not missing value then
308
- ${(0, scripts_1.senderSnippet)('theMsg', ' ')}
276
+ ${(0, scripts_1.senderSnippet)(' ')}
309
277
  set attNames to {}
310
278
  try
311
279
  set attNames to name of every attachment of theMsg
312
280
  end try
313
- set bodyText to ""
314
- try
315
- set bodyText to (plain text content of theMsg) as string
316
- end try
317
- set out to out & ${(0, run_1.asRow)([
318
- '(id of theMsg as string)',
319
- 'sndName',
320
- 'sndAddr',
321
- 'my sanitizeList(attNames)',
322
- 'bodyText',
323
- ])}
281
+ ${body}
282
+ set out to out & ${(0, run_1.asRow)(['(id of theMsg as string)', 'sndName', 'sndAddr', 'my sanitizePositionalList(attNames)', 'bodyText'])}
324
283
  end if
325
284
  end repeat
326
285
  return out
327
- end tell`;
328
- const details = new Map();
329
- for (const record of (0, run_1.splitRecords)(await (0, run_1.runOsaScript)(detailScript, 300000))) {
286
+ end tell`, 'scan');
287
+ const details = new Map((0, run_1.splitRecords)(raw).map(record => {
330
288
  const parts = (0, run_1.splitFields)(record);
331
- details.set((0, run_1.field)(parts, 0), {
289
+ const detail = {
332
290
  senderName: (0, run_1.field)(parts, 1),
333
291
  senderEmail: (0, run_1.field)(parts, 2),
334
- attachmentNames: (0, run_1.splitList)((0, run_1.field)(parts, 3)),
292
+ attachmentNames: (0, run_1.splitPositionalList)((0, run_1.field)(parts, 3)),
335
293
  body: (0, run_1.field)(parts, 4),
336
- });
337
- }
294
+ };
295
+ return [(0, run_1.field)(parts, 0), detail];
296
+ }));
338
297
  const matches = [];
339
298
  for (const candidate of candidates) {
340
299
  const detail = details.get(candidate.id);
341
300
  if (!detail)
342
301
  continue;
343
- // requireAttachment can only be answered once the attachment names are in
344
- // hand, so it filters here rather than during the index pass.
345
- if (filter.requireAttachment && detail.attachmentNames.length === 0)
302
+ // Attachment names only arrive with the details, so this test waits for them.
303
+ if (request.requireAttachment && detail.attachmentNames.length === 0)
346
304
  continue;
347
305
  matches.push({
348
306
  entryId: candidate.id,
349
- storeId: '', // macOS AppleScript has no StoreID equivalent
307
+ storeId: '',
350
308
  subject: candidate.subject,
351
309
  senderName: detail.senderName,
352
310
  senderEmail: detail.senderEmail,
@@ -359,50 +317,74 @@ end tell`;
359
317
  return matches;
360
318
  }
361
319
  /**
362
- * Read the email currently selected (or open) in Outlook — full body,
363
- * attachments listed by name but not saved.
320
+ * The email currently selected — or open — in Outlook. `current messages`
321
+ * covers both cases the Windows reader handles separately.
364
322
  *
365
- * `current messages` covers both cases the Windows reader handles separately
366
- * (an explorer selection and an open item), so there is no second lookup here.
323
+ * Where it sits comes back as a second record, so the detail row keeps its body
324
+ * last. A mailbox with no account object answers `account of` with missing
325
+ * value; its root folder's id is then looked up in the profile database.
367
326
  */
368
327
  async function readSelectedEmail() {
369
- const script = `tell application "Microsoft Outlook"
328
+ const records = (0, run_1.splitRecords)(await (0, run_1.runOsaScript)(`tell application "Microsoft Outlook"
370
329
  set sel to {}
371
330
  try
372
331
  set sel to current messages
373
332
  end try
374
- if (count of sel) is 0 then error "No email is selected in Outlook. Open Outlook, select (or open) an email, then try again."
333
+ if (count of sel) is 0 then error ${(0, run_1.asString)(`${(0, errors_1.failureTag)('NOT_FOUND', 'email')}No email is selected in Outlook. Select or open an email, then try again.`)}
375
334
  set theMsg to item 1 of sel
376
335
  ${(0, scripts_1.messageDetailSnippet)(true)}
377
- return ${(0, run_1.asRow)((0, scripts_1.messageDetailFields)(true))}
378
- end tell`;
379
- const records = (0, run_1.splitRecords)(await (0, run_1.runOsaScript)(script, 30000));
380
- if (records.length === 0) {
381
- throw new errors_1.NotFoundError('email', 'Failed to read the selected Outlook email.');
382
- }
336
+ set acctAddr to ""
337
+ try
338
+ set rawAddr to email address of (account of theMsg)
339
+ if rawAddr is not missing value then set acctAddr to rawAddr as string
340
+ end try
341
+ set chainNames to {}
342
+ set rootId to ""
343
+ set theFolder to missing value
344
+ try
345
+ set theFolder to folder of theMsg
346
+ end try
347
+ if theFolder is not missing value then
348
+ ${(0, scripts_1.folderChainSnippet)(' ')}
349
+ try
350
+ set rootId to (id of cur) as string
351
+ end try
352
+ end if
353
+ return ${(0, run_1.asRow)((0, scripts_1.messageDetailFields)(true))} & ${(0, run_1.asRow)(['acctAddr', 'my sanitizeList(chainNames)', 'rootId'])}
354
+ end tell`, 'quick'));
355
+ if (records.length === 0)
356
+ throw new errors_1.NotFoundError('email', 'Outlook reported no selected email.');
383
357
  const parts = (0, run_1.splitFields)(records[0]);
358
+ const where = (0, run_1.splitFields)(records[1] ?? '');
359
+ const chain = (0, run_1.splitList)((0, run_1.field)(where, 1)).reverse();
360
+ const account = (0, run_1.field)(where, 0) || (chain.length ? await accountByRootFolder((0, run_1.field)(where, 2)) : '');
384
361
  return {
385
362
  entryId: (0, run_1.field)(parts, scripts_1.MessageDetail.id),
386
- storeId: '', // macOS AppleScript has no StoreID equivalent
363
+ storeId: '',
387
364
  subject: (0, run_1.field)(parts, scripts_1.MessageDetail.subject).trim(),
388
365
  senderName: (0, run_1.field)(parts, scripts_1.MessageDetail.senderName),
389
366
  senderEmail: (0, run_1.field)(parts, scripts_1.MessageDetail.senderEmail),
390
367
  receivedTime: (0, run_1.field)(parts, scripts_1.MessageDetail.receivedTime),
391
368
  body: (0, run_1.field)(parts, scripts_1.MessageDetail.body),
392
- attachmentNames: (0, run_1.splitList)((0, run_1.field)(parts, scripts_1.MessageDetail.attachmentNames)),
369
+ attachmentNames: (0, run_1.splitPositionalList)((0, run_1.field)(parts, scripts_1.MessageDetail.attachmentNames)),
370
+ folderPath: account && chain.length ? (0, scripts_1.macFolderPath)(account, chain[0], chain.slice(1)) : '',
393
371
  };
394
372
  }
395
- /** Open an email in Outlook by its message id, and bring Outlook forward.
396
- * `_storeId` is accepted for parity with Windows and ignored — macOS
397
- * AppleScript has no StoreID (see `readEmailBody`). */
398
- async function openOutlookEmail(entryId, _storeId) {
399
- const id = (0, scripts_1.macMessageId)(entryId);
400
- const script = `
373
+ /** The address of the account whose root folder has this id, per the profile database; '' if none. */
374
+ async function accountByRootFolder(rootId) {
375
+ if (!/^\d+$/.test(rootId))
376
+ return '';
377
+ const id = Number(rootId);
378
+ return (await (0, profile_1.readProfileAccounts)()).find(a => a.folderIds['root folder'] === id)?.emailAccount ?? '';
379
+ }
380
+ /** Open an email in Outlook and bring Outlook forward. */
381
+ async function openOutlookEmail(email) {
382
+ const id = (0, scripts_1.macMessageId)(email.entryId);
383
+ await (0, run_1.runOsaScript)(`
401
384
  tell application "Microsoft Outlook"
402
385
  ${(0, scripts_1.messageLookupSnippet)(id)}
403
386
  open theMsg
404
387
  activate
405
- end tell`;
406
- await (0, run_1.runOsaScript)(script, 30000);
388
+ end tell`, 'quick');
407
389
  }
408
390
  //# sourceMappingURL=read.js.map