@dawnlit/outlook-bridge 2.1.0 → 2.2.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 (68) hide show
  1. package/README.md +11 -6
  2. package/dist/errors.d.ts +17 -6
  3. package/dist/errors.js +16 -7
  4. package/dist/errors.js.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.js +3 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/mac/accounts.d.ts +4 -0
  9. package/dist/mac/accounts.js +10 -0
  10. package/dist/mac/accounts.js.map +1 -1
  11. package/dist/mac/bounces.js +10 -19
  12. package/dist/mac/bounces.js.map +1 -1
  13. package/dist/mac/cleanup.d.ts +11 -3
  14. package/dist/mac/cleanup.js +26 -17
  15. package/dist/mac/cleanup.js.map +1 -1
  16. package/dist/mac/drafts.js +3 -16
  17. package/dist/mac/drafts.js.map +1 -1
  18. package/dist/mac/folders.d.ts +5 -0
  19. package/dist/mac/folders.js +14 -5
  20. package/dist/mac/folders.js.map +1 -1
  21. package/dist/mac/profile.d.ts +12 -0
  22. package/dist/mac/profile.js +75 -1
  23. package/dist/mac/profile.js.map +1 -1
  24. package/dist/mac/read.js +14 -26
  25. package/dist/mac/read.js.map +1 -1
  26. package/dist/mac/run.d.ts +0 -3
  27. package/dist/mac/run.js +8 -8
  28. package/dist/mac/run.js.map +1 -1
  29. package/dist/mac/scripts.d.ts +46 -0
  30. package/dist/mac/scripts.js +98 -10
  31. package/dist/mac/scripts.js.map +1 -1
  32. package/dist/mac/send.d.ts +2 -1
  33. package/dist/mac/send.js +9 -3
  34. package/dist/mac/send.js.map +1 -1
  35. package/dist/mac/templates.js +1 -4
  36. package/dist/mac/templates.js.map +1 -1
  37. package/dist/mail.d.ts +6 -0
  38. package/dist/mail.js +7 -1
  39. package/dist/mail.js.map +1 -1
  40. package/dist/types.d.ts +20 -4
  41. package/dist/types.js.map +1 -1
  42. package/dist/windows/accounts.js +1 -1
  43. package/dist/windows/accounts.js.map +1 -1
  44. package/dist/windows/attachments.js +2 -2
  45. package/dist/windows/attachments.js.map +1 -1
  46. package/dist/windows/bounces.js +3 -3
  47. package/dist/windows/bounces.js.map +1 -1
  48. package/dist/windows/cleanup.js +2 -4
  49. package/dist/windows/cleanup.js.map +1 -1
  50. package/dist/windows/drafts.js +2 -2
  51. package/dist/windows/drafts.js.map +1 -1
  52. package/dist/windows/folders.js +1 -1
  53. package/dist/windows/folders.js.map +1 -1
  54. package/dist/windows/json.js.map +1 -0
  55. package/dist/windows/read.js +17 -14
  56. package/dist/windows/read.js.map +1 -1
  57. package/dist/windows/scripts.d.ts +6 -0
  58. package/dist/windows/scripts.js +11 -2
  59. package/dist/windows/scripts.js.map +1 -1
  60. package/dist/windows/send.d.ts +5 -1
  61. package/dist/windows/send.js +14 -4
  62. package/dist/windows/send.js.map +1 -1
  63. package/dist/windows/templates.js +2 -2
  64. package/dist/windows/templates.js.map +1 -1
  65. package/package.json +1 -1
  66. package/dist/shared/json.js.map +0 -1
  67. /package/dist/{shared → windows}/json.d.ts +0 -0
  68. /package/dist/{shared → windows}/json.js +0 -0
@@ -8,9 +8,17 @@ const scripts_1 = require("./scripts");
8
8
  const mail_1 = require("../mail");
9
9
  /**
10
10
  * Delete mail by message id from anywhere in the account, into Deleted Items.
11
- * Inbox and Sent Items (and their subfolders) are refused unless
12
- * `allowProtected`; `dryRun` reports without deleting; every outcome names the
13
- * subject and folder of the item that actually resolved.
11
+ * The same four things hold the line as on Windows:
12
+ * - An id that resolves outside this account is refused, whatever
13
+ * `allowProtected` says. A message id resolves against the whole
14
+ * application, and the Inbox/Sent check below only knows this account's
15
+ * folders by name, so another mailbox's mail would otherwise pass it.
16
+ * - Inbox and Sent Items (and their subfolders) are refused unless
17
+ * `allowProtected`.
18
+ * - `dryRun` reports without deleting.
19
+ * - Every outcome names the subject and folder of the item that actually
20
+ * resolved — the folder only once the item is known to be this account's,
21
+ * since the path is built from this account's address.
14
22
  */
15
23
  async function deleteOutlookEmails(request) {
16
24
  const { valid, invalid } = (0, scripts_1.partitionMessageIds)(request.entryIds);
@@ -23,21 +31,23 @@ async function deleteOutlookEmails(request) {
23
31
  }));
24
32
  if (valid.length > 0) {
25
33
  const acct = await (0, scripts_1.resolveMacAccount)(request.account);
34
+ // The protected roots' names, read off this account: the roots are the
35
+ // ones Windows protects, from the one list both read.
36
+ const readProtectedNames = mail_1.PROTECTED_ROOTS
37
+ .map(rootId => `${(0, scripts_1.rootFolderSnippet)(acct, scripts_1.MAC_ROOT_TERMS[rootId], 'protectedFolder')}
38
+ set end of protectedNames to (name of protectedFolder) as string`)
39
+ .join('\n');
26
40
  const raw = await (0, run_1.runOsaScript)(`tell application "Microsoft Outlook"
27
41
  ${(0, scripts_1.accountLookupSnippet)(acct)}
28
- ${(0, scripts_1.rootFolderSnippet)(acct, 'inbox', 'inboxFolder')}
29
- ${(0, scripts_1.rootFolderSnippet)(acct, 'sent items', 'sentFolder')}
30
- set inboxName to (name of inboxFolder) as string
31
- set sentName to (name of sentFolder) as string
42
+ ${(0, scripts_1.accountRootIdSnippet)(acct)}
43
+ set protectedNames to {}
44
+ ${readProtectedNames}
32
45
  set out to ""
33
46
  repeat with theId in ${(0, run_1.asIdList)(valid)}
34
47
  set subj to ""
35
48
  set pathText to ""
36
49
  try
37
- set theMsg to missing value
38
- try
39
- set theMsg to message id theId
40
- end try
50
+ ${(0, scripts_1.messageByIdSnippet)(' ')}
41
51
  if theMsg is missing value then error "no item with that message id in this Outlook"
42
52
  try
43
53
  set subj to (subject of theMsg) as string
@@ -48,10 +58,11 @@ ${(0, scripts_1.rootFolderSnippet)(acct, 'sent items', 'sentFolder')}
48
58
  end try
49
59
  if theFolder is missing value then error "item has no parent folder - refusing to delete"
50
60
  ${(0, scripts_1.folderChainSnippet)(' ')}
61
+ ${(0, scripts_1.ownMailGuardSnippet)(request.account, 'delete', ' ')}
51
62
  set pathText to my sanitizeList(my reverseList(chainNames))
52
63
  -- The chain's top is what extends the refusal to Inbox and Sent Items SUBFOLDERS.
53
64
  set rootName to item (count of chainNames) of chainNames
54
- set isProtected to (rootName is inboxName) or (rootName is sentName)
65
+ set isProtected to (protectedNames contains rootName)
55
66
  if isProtected and not ${(0, run_1.asBool)(request.allowProtected)} then
56
67
  set out to out & ${(0, run_1.asRow)(['(theId as string)', 'subj', 'pathText', '"refused"', (0, run_1.asString)(mail_1.PROTECTED_MAIL_REASON)])}
57
68
  else if ${(0, run_1.asBool)(request.dryRun)} then
@@ -105,12 +116,13 @@ ${(0, scripts_1.rootFolderSnippet)(acct, 'deleted items', 'trashFolder')}
105
116
  on error
106
117
  set timeList to {}
107
118
  end try
119
+ set hasTimes to ((count of timeList) is (count of idList))
108
120
  set olderThanDays to ${(0, run_1.asInt)(request.olderThanDays)}
109
121
  set cutoff to (current date) - (olderThanDays * days)
110
122
  set out to ""
111
123
  repeat with i from 1 to (count of idList)
112
124
  set stamp to missing value
113
- if (count of timeList) is (count of idList) then set stamp to item i of timeList
125
+ if hasTimes then set stamp to item i of timeList
114
126
  set keepIt to false
115
127
  if olderThanDays > 0 and stamp is not missing value and stamp is greater than cutoff then set keepIt to true
116
128
  set out to out & ${(0, run_1.asRow)(['(item i of idList as string)', '(keepIt as string)'])}
@@ -131,10 +143,7 @@ end tell`, 'scan');
131
143
  set failedCount to 0
132
144
  repeat with theId in ${(0, run_1.asIdList)(doomed.map(row => row.id))}
133
145
  try
134
- set theMsg to missing value
135
- try
136
- set theMsg to message id theId
137
- end try
146
+ ${(0, scripts_1.messageByIdSnippet)(' ')}
138
147
  if theMsg is missing value then error "gone"
139
148
  permanently delete theMsg
140
149
  set purgedCount to purgedCount + 1
@@ -1 +1 @@
1
- {"version":3,"file":"cleanup.js","sourceRoot":"","sources":["../../src/mac/cleanup.ts"],"names":[],"mappings":"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","sourcesContent":["// Deleting mail, and emptying what was already deleted.\nimport {\n asBool,\n asIdList,\n asInt,\n asRow,\n asString,\n boolField,\n field,\n intField,\n runOsaScript,\n splitFields,\n splitList,\n splitRecords,\n summaryFields,\n} from './run';\nimport {\n accountLookupSnippet,\n folderChainSnippet,\n macFolderPath,\n partitionMessageIds,\n resolveMacAccount,\n rootFolderSnippet,\n} from './scripts';\nimport { deleteMailResult, deleteStatus, PROTECTED_MAIL_REASON } from '../mail';\nimport type { DeleteEmailsRequest, PurgeRequest } from '../backend';\nimport type { DeleteMailOutcome, DeleteMailResult, PurgeDeletedItemsResult } from '../types';\n\n/**\n * Delete mail by message id from anywhere in the account, into Deleted Items.\n * Inbox and Sent Items (and their subfolders) are refused unless\n * `allowProtected`; `dryRun` reports without deleting; every outcome names the\n * subject and folder of the item that actually resolved.\n */\nexport async function deleteOutlookEmails(request: DeleteEmailsRequest): Promise<DeleteMailResult> {\n const { valid, invalid } = partitionMessageIds(request.entryIds);\n const items: DeleteMailOutcome[] = invalid.map(bad => ({\n entryId: bad.entryId,\n subject: '',\n folderPath: '',\n status: 'failed',\n reason: bad.error,\n }));\n if (valid.length > 0) {\n const acct = await resolveMacAccount(request.account);\n const raw = await runOsaScript(`tell application \"Microsoft Outlook\"\n${accountLookupSnippet(acct)}\n${rootFolderSnippet(acct, 'inbox', 'inboxFolder')}\n${rootFolderSnippet(acct, 'sent items', 'sentFolder')}\n set inboxName to (name of inboxFolder) as string\n set sentName to (name of sentFolder) as string\n set out to \"\"\n repeat with theId in ${asIdList(valid)}\n set subj to \"\"\n set pathText to \"\"\n try\n set theMsg to missing value\n try\n set theMsg to message id theId\n end try\n if theMsg is missing value then error \"no item with that message id in this Outlook\"\n try\n set subj to (subject of theMsg) as string\n end try\n set theFolder to missing value\n try\n set theFolder to folder of theMsg\n end try\n if theFolder is missing value then error \"item has no parent folder - refusing to delete\"\n${folderChainSnippet(' ')}\n set pathText to my sanitizeList(my reverseList(chainNames))\n -- The chain's top is what extends the refusal to Inbox and Sent Items SUBFOLDERS.\n set rootName to item (count of chainNames) of chainNames\n set isProtected to (rootName is inboxName) or (rootName is sentName)\n if isProtected and not ${asBool(request.allowProtected)} then\n set out to out & ${asRow(['(theId as string)', 'subj', 'pathText', '\"refused\"', asString(PROTECTED_MAIL_REASON)])}\n else if ${asBool(request.dryRun)} then\n set out to out & ${asRow(['(theId as string)', 'subj', 'pathText', '\"would-delete\"', '\"\"'])}\n else\n delete theMsg\n set out to out & ${asRow(['(theId as string)', 'subj', 'pathText', '\"deleted\"', '\"\"'])}\n end if\n on error errText\n set out to out & ${asRow(['(theId as string)', 'subj', 'pathText', '\"failed\"', 'errText'])}\n end try\n end repeat\n return out\nend tell\n\non reverseList(lst)\n set out to {}\n repeat with i from (count of lst) to 1 by -1\n set end of out to item i of lst\n end repeat\n return out\nend reverseList`, 'scan');\n\n for (const record of splitRecords(raw)) {\n const parts = splitFields(record);\n const segments = splitList(field(parts, 2));\n items.push({\n entryId: field(parts, 0),\n subject: field(parts, 1).trim(),\n folderPath: segments.length ? macFolderPath(request.account, segments[0], segments.slice(1)) : '',\n status: deleteStatus(field(parts, 3)),\n reason: field(parts, 4),\n });\n }\n }\n return deleteMailResult(request.dryRun, items);\n}\n\n/**\n * Permanently remove items from the account's Deleted Items. The folder is\n * indexed first and items destroyed by id afterwards, so deleting can't shift\n * the collection out from under the walk.\n */\nexport async function purgeDeletedItems(request: PurgeRequest): Promise<PurgeDeletedItemsResult> {\n const folderPath = macFolderPath(request.account, 'Deleted Items', []);\n const acct = await resolveMacAccount(request.account);\n const index = await runOsaScript(`tell application \"Microsoft Outlook\"\n${accountLookupSnippet(acct)}\n${rootFolderSnippet(acct, 'deleted items', 'trashFolder')}\n set idList to id of every message of trashFolder\n try\n set timeList to time received of every message of trashFolder\n on error\n set timeList to {}\n end try\n set olderThanDays to ${asInt(request.olderThanDays)}\n set cutoff to (current date) - (olderThanDays * days)\n set out to \"\"\n repeat with i from 1 to (count of idList)\n set stamp to missing value\n if (count of timeList) is (count of idList) then set stamp to item i of timeList\n set keepIt to false\n if olderThanDays > 0 and stamp is not missing value and stamp is greater than cutoff then set keepIt to true\n set out to out & ${asRow(['(item i of idList as string)', '(keepIt as string)'])}\n end repeat\n return out\nend tell`, 'scan');\n\n const rows = splitRecords(index).map(record => {\n const parts = splitFields(record);\n return { id: field(parts, 0), keep: boolField(parts, 1) };\n });\n const doomed = rows.filter(row => !row.keep);\n const kept = rows.length - doomed.length;\n if (request.dryRun || doomed.length === 0) {\n return { folderPath, dryRun: request.dryRun, matched: doomed.length, purged: 0, kept, failed: 0 };\n }\n\n const summary = summaryFields(await runOsaScript(`tell application \"Microsoft Outlook\"\n set purgedCount to 0\n set failedCount to 0\n repeat with theId in ${asIdList(doomed.map(row => row.id))}\n try\n set theMsg to missing value\n try\n set theMsg to message id theId\n end try\n if theMsg is missing value then error \"gone\"\n permanently delete theMsg\n set purgedCount to purgedCount + 1\n on error\n set failedCount to failedCount + 1\n end try\n end repeat\n return ${asRow(['(purgedCount as string)', '(failedCount as string)'])}\nend tell`, 'purge'));\n return {\n folderPath,\n dryRun: request.dryRun,\n matched: doomed.length,\n purged: intField(summary, 0),\n kept,\n failed: intField(summary, 1),\n };\n}\n"]}
1
+ {"version":3,"file":"cleanup.js","sourceRoot":"","sources":["../../src/mac/cleanup.ts"],"names":[],"mappings":"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","sourcesContent":["// Deleting mail, and emptying what was already deleted.\nimport {\n asBool,\n asIdList,\n asInt,\n asRow,\n asString,\n boolField,\n field,\n intField,\n runOsaScript,\n splitFields,\n splitList,\n splitRecords,\n summaryFields,\n} from './run';\nimport {\n accountLookupSnippet,\n accountRootIdSnippet,\n folderChainSnippet,\n MAC_ROOT_TERMS,\n macFolderPath,\n messageByIdSnippet,\n ownMailGuardSnippet,\n partitionMessageIds,\n resolveMacAccount,\n rootFolderSnippet,\n} from './scripts';\nimport { deleteMailResult, deleteStatus, PROTECTED_MAIL_REASON, PROTECTED_ROOTS } from '../mail';\nimport type { DeleteEmailsRequest, PurgeRequest } from '../backend';\nimport type { DeleteMailOutcome, DeleteMailResult, PurgeDeletedItemsResult } from '../types';\n\n/**\n * Delete mail by message id from anywhere in the account, into Deleted Items.\n * The same four things hold the line as on Windows:\n * - An id that resolves outside this account is refused, whatever\n * `allowProtected` says. A message id resolves against the whole\n * application, and the Inbox/Sent check below only knows this account's\n * folders by name, so another mailbox's mail would otherwise pass it.\n * - Inbox and Sent Items (and their subfolders) are refused unless\n * `allowProtected`.\n * - `dryRun` reports without deleting.\n * - Every outcome names the subject and folder of the item that actually\n * resolved — the folder only once the item is known to be this account's,\n * since the path is built from this account's address.\n */\nexport async function deleteOutlookEmails(request: DeleteEmailsRequest): Promise<DeleteMailResult> {\n const { valid, invalid } = partitionMessageIds(request.entryIds);\n const items: DeleteMailOutcome[] = invalid.map(bad => ({\n entryId: bad.entryId,\n subject: '',\n folderPath: '',\n status: 'failed',\n reason: bad.error,\n }));\n if (valid.length > 0) {\n const acct = await resolveMacAccount(request.account);\n // The protected roots' names, read off this account: the roots are the\n // ones Windows protects, from the one list both read.\n const readProtectedNames = PROTECTED_ROOTS\n .map(rootId => `${rootFolderSnippet(acct, MAC_ROOT_TERMS[rootId], 'protectedFolder')}\n set end of protectedNames to (name of protectedFolder) as string`)\n .join('\\n');\n const raw = await runOsaScript(`tell application \"Microsoft Outlook\"\n${accountLookupSnippet(acct)}\n${accountRootIdSnippet(acct)}\n set protectedNames to {}\n${readProtectedNames}\n set out to \"\"\n repeat with theId in ${asIdList(valid)}\n set subj to \"\"\n set pathText to \"\"\n try\n${messageByIdSnippet(' ')}\n if theMsg is missing value then error \"no item with that message id in this Outlook\"\n try\n set subj to (subject of theMsg) as string\n end try\n set theFolder to missing value\n try\n set theFolder to folder of theMsg\n end try\n if theFolder is missing value then error \"item has no parent folder - refusing to delete\"\n${folderChainSnippet(' ')}\n${ownMailGuardSnippet(request.account, 'delete', ' ')}\n set pathText to my sanitizeList(my reverseList(chainNames))\n -- The chain's top is what extends the refusal to Inbox and Sent Items SUBFOLDERS.\n set rootName to item (count of chainNames) of chainNames\n set isProtected to (protectedNames contains rootName)\n if isProtected and not ${asBool(request.allowProtected)} then\n set out to out & ${asRow(['(theId as string)', 'subj', 'pathText', '\"refused\"', asString(PROTECTED_MAIL_REASON)])}\n else if ${asBool(request.dryRun)} then\n set out to out & ${asRow(['(theId as string)', 'subj', 'pathText', '\"would-delete\"', '\"\"'])}\n else\n delete theMsg\n set out to out & ${asRow(['(theId as string)', 'subj', 'pathText', '\"deleted\"', '\"\"'])}\n end if\n on error errText\n set out to out & ${asRow(['(theId as string)', 'subj', 'pathText', '\"failed\"', 'errText'])}\n end try\n end repeat\n return out\nend tell\n\non reverseList(lst)\n set out to {}\n repeat with i from (count of lst) to 1 by -1\n set end of out to item i of lst\n end repeat\n return out\nend reverseList`, 'scan');\n\n for (const record of splitRecords(raw)) {\n const parts = splitFields(record);\n const segments = splitList(field(parts, 2));\n items.push({\n entryId: field(parts, 0),\n subject: field(parts, 1).trim(),\n folderPath: segments.length ? macFolderPath(request.account, segments[0], segments.slice(1)) : '',\n status: deleteStatus(field(parts, 3)),\n reason: field(parts, 4),\n });\n }\n }\n return deleteMailResult(request.dryRun, items);\n}\n\n/**\n * Permanently remove items from the account's Deleted Items. The folder is\n * indexed first and items destroyed by id afterwards, so deleting can't shift\n * the collection out from under the walk.\n */\nexport async function purgeDeletedItems(request: PurgeRequest): Promise<PurgeDeletedItemsResult> {\n const folderPath = macFolderPath(request.account, 'Deleted Items', []);\n const acct = await resolveMacAccount(request.account);\n const index = await runOsaScript(`tell application \"Microsoft Outlook\"\n${accountLookupSnippet(acct)}\n${rootFolderSnippet(acct, 'deleted items', 'trashFolder')}\n set idList to id of every message of trashFolder\n try\n set timeList to time received of every message of trashFolder\n on error\n set timeList to {}\n end try\n set hasTimes to ((count of timeList) is (count of idList))\n set olderThanDays to ${asInt(request.olderThanDays)}\n set cutoff to (current date) - (olderThanDays * days)\n set out to \"\"\n repeat with i from 1 to (count of idList)\n set stamp to missing value\n if hasTimes then set stamp to item i of timeList\n set keepIt to false\n if olderThanDays > 0 and stamp is not missing value and stamp is greater than cutoff then set keepIt to true\n set out to out & ${asRow(['(item i of idList as string)', '(keepIt as string)'])}\n end repeat\n return out\nend tell`, 'scan');\n\n const rows = splitRecords(index).map(record => {\n const parts = splitFields(record);\n return { id: field(parts, 0), keep: boolField(parts, 1) };\n });\n const doomed = rows.filter(row => !row.keep);\n const kept = rows.length - doomed.length;\n if (request.dryRun || doomed.length === 0) {\n return { folderPath, dryRun: request.dryRun, matched: doomed.length, purged: 0, kept, failed: 0 };\n }\n\n const summary = summaryFields(await runOsaScript(`tell application \"Microsoft Outlook\"\n set purgedCount to 0\n set failedCount to 0\n repeat with theId in ${asIdList(doomed.map(row => row.id))}\n try\n${messageByIdSnippet(' ')}\n if theMsg is missing value then error \"gone\"\n permanently delete theMsg\n set purgedCount to purgedCount + 1\n on error\n set failedCount to failedCount + 1\n end try\n end repeat\n return ${asRow(['(purgedCount as string)', '(failedCount as string)'])}\nend tell`, 'purge'));\n return {\n folderPath,\n dryRun: request.dryRun,\n matched: doomed.length,\n purged: intField(summary, 0),\n kept,\n failed: intField(summary, 1),\n };\n}\n"]}
@@ -20,13 +20,6 @@ const scripts_1 = require("./scripts");
20
20
  /** The account's drafts, newest first. Bodies are previewed, never returned whole. */
21
21
  async function listOutlookDrafts(request) {
22
22
  const acct = await (0, scripts_1.resolveMacAccount)(request.account);
23
- const preview = request.previewChars > 0
24
- ? ` set bodyText to ""
25
- try
26
- set bodyText to (plain text content of theMsg) as string
27
- end try
28
- 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`
29
- : ' set bodyText to ""';
30
23
  const raw = await (0, run_1.runOsaScript)(`tell application "Microsoft Outlook"
31
24
  ${(0, scripts_1.accountLookupSnippet)(acct)}
32
25
  ${(0, scripts_1.rootFolderSnippet)(acct, 'drafts', 'draftsFolder')}
@@ -56,7 +49,7 @@ ${(0, scripts_1.rootFolderSnippet)(acct, 'drafts', 'draftsFolder')}
56
49
  end try
57
50
  end repeat
58
51
  end try
59
- ${preview}
52
+ ${(0, scripts_1.bodyTextSnippet)(' ', request.previewChars)}
60
53
  set attCount to 0
61
54
  try
62
55
  set attCount to count of attachments of theMsg
@@ -136,10 +129,7 @@ ${(0, scripts_1.rootFolderSnippet)(acct, 'drafts', 'draftsFolder')}
136
129
  repeat with theId in ${(0, run_1.asIdList)(valid)}
137
130
  set subj to ""
138
131
  try
139
- set theMsg to missing value
140
- try
141
- set theMsg to message id theId
142
- end try
132
+ ${(0, scripts_1.messageByIdSnippet)(' ')}
143
133
  if theMsg is missing value then error "no such item in this account's Drafts folder"
144
134
  try
145
135
  set subj to (subject of theMsg) as string
@@ -187,10 +177,7 @@ ${(0, scripts_1.rootFolderSnippet)(acct, 'drafts', 'draftsFolder')}
187
177
  repeat with theId in wanted
188
178
  set subj to ""
189
179
  try
190
- set theMsg to missing value
191
- try
192
- set theMsg to message id theId
193
- end try
180
+ ${(0, scripts_1.messageByIdSnippet)(' ')}
194
181
  if theMsg is missing value then error "draft is no longer in the Drafts folder"
195
182
  ${boundElsewhereSnippet(request.account, ' ')}
196
183
  if not boundElsewhere then
@@ -1 +1 @@
1
- {"version":3,"file":"drafts.js","sourceRoot":"","sources":["../../src/mac/drafts.ts"],"names":[],"mappings":"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","sourcesContent":["// Drafts: which ones belong to an account, and listing, sending or deleting them.\n//\n// Where Windows scans two Drafts folders and proves each item's sending\n// account, macOS resolves the one Drafts folder belonging to the account —\n// through the account object, or by the folder's own id for a mailbox the\n// dictionary won't name (see profile.ts) — so a draft found there belongs to\n// the account by construction.\n//\n// What the folder can't vouch for is an item's own binding, which is why the\n// guards compare against the requested address as a literal rather than asking\n// `targetAcct`: there may be no account object to ask.\nimport {\n asIdList,\n asInt,\n asRow,\n asString,\n field,\n intField,\n ITEM_FAILURE_ROW,\n runOsaScript,\n splitBatch,\n splitFields,\n splitList,\n splitRecords,\n} from './run';\nimport {\n accountLookupSnippet,\n macFolderPath,\n partitionMessageIds,\n resolveMacAccount,\n rootFolderSnippet,\n} from './scripts';\nimport type { AccountRequest, EntryIdsRequest, ListDraftsRequest } from '../backend';\nimport type { DeleteDraftsResult, ItemFailure, ListDraftsResult, SendDraftsResult } from '../types';\n\n/** The account's drafts, newest first. Bodies are previewed, never returned whole. */\nexport async function listOutlookDrafts(request: ListDraftsRequest): Promise<ListDraftsResult> {\n const acct = await resolveMacAccount(request.account);\n const preview = request.previewChars > 0\n ? ` set bodyText to \"\"\n try\n set bodyText to (plain text content of theMsg) as string\n end try\n if (length of bodyText) > ${asInt(request.previewChars)} then set bodyText to text 1 thru ${asInt(request.previewChars)} of bodyText`\n : ' set bodyText to \"\"';\n const raw = await runOsaScript(`tell application \"Microsoft Outlook\"\n${accountLookupSnippet(acct)}\n${rootFolderSnippet(acct, 'drafts', 'draftsFolder')}\n set out to \"\"\n repeat with theMsg in (messages of draftsFolder)\n set subj to \"\"\n try\n set subj to (subject of theMsg) as string\n end try\n set toNames to {}\n set toAddrs to {}\n try\n repeat with r in (every to recipient of theMsg)\n try\n set ea to email address of r\n set oneAddr to \"\"\n set oneName to \"\"\n try\n set oneAddr to (address of ea) as string\n end try\n try\n set oneName to (name of ea) as string\n end try\n if oneName is \"\" then set oneName to oneAddr\n if oneName is not \"\" then set end of toNames to oneName\n if oneAddr is not \"\" then set end of toAddrs to oneAddr\n end try\n end repeat\n end try\n${preview}\n set attCount to 0\n try\n set attCount to count of attachments of theMsg\n end try\n set modAt to \"\"\n try\n set modAt to my isoDate(modification date of theMsg)\n end try\n${boundElsewhereSnippet(request.account, ' ')}\n if not boundElsewhere then set out to out & ${asRow(['(id of theMsg as string)', 'subj', 'my joinList(toNames, \", \")', 'my sanitizeList(toAddrs)', 'bodyText', '(attCount as string)', 'modAt'])}\n end repeat\n return out\nend tell`, 'standard');\n\n const folderPath = macFolderPath(request.account, 'Drafts', []);\n const drafts = splitRecords(raw).map(record => {\n const parts = splitFields(record);\n return {\n entryId: field(parts, 0),\n subject: field(parts, 1),\n to: field(parts, 2),\n toEmails: splitList(field(parts, 3)),\n bodyPreview: field(parts, 4).replace(/\\s+/g, ' ').trim(),\n hasAttachments: intField(parts, 5) > 0,\n lastModified: field(parts, 6),\n folderPath,\n };\n });\n drafts.sort((a, b) => b.lastModified.localeCompare(a.lastModified));\n return {\n account: request.account,\n foldersScanned: [folderPath],\n count: drafts.length,\n truncated: drafts.length > request.limit,\n drafts: drafts.slice(0, request.limit),\n };\n}\n\n/**\n * Set `boundElsewhere` to whether `theMsg`, a message in the account's Drafts\n * folder, is set to send from some OTHER account. An unbound draft there is the\n * account's own. Listing, sending all and the per-id guard all apply this one\n * rule, as Windows' Test-DraftMatches is applied to all three.\n */\nfunction boundElsewhereSnippet(emailAccount: string, indent: string): string {\n return `${indent}set boundTo to \"\"\n${indent}try\n${indent} set boundTo to (email address of (account of theMsg)) as string\n${indent}end try\n${indent}set boundElsewhere to (boundTo is not \"\" and boundTo is not ${asString(emailAccount)})`;\n}\n\n/**\n * The AppleScript that proves `theMsg` is one of this account's drafts before\n * `verb` happens to it: it sits in the account's Drafts folder, and it is bound\n * to this account or to none.\n */\nfunction ownDraftGuard(emailAccount: string, verb: 'send' | 'delete'): string {\n return ` set inFolder to \"\"\n try\n set inFolder to (name of (folder of theMsg)) as string\n end try\n if inFolder is not draftsName or draftIds does not contain (id of theMsg) then error \"item is not in this account's Drafts folder - refusing to ${verb}\"\n${boundElsewhereSnippet(emailAccount, ' ')}\n if boundElsewhere then error \"draft is not bound to this account - refusing to ${verb}\"`;\n}\n\n/** Run `verb` over named drafts of the account, each proved to be its own first. */\nasync function actOnDrafts(\n request: EntryIdsRequest,\n verb: 'send' | 'delete',\n): Promise<{ done: number; failed: ItemFailure[] }> {\n const { valid, invalid } = partitionMessageIds(request.entryIds);\n if (valid.length === 0) return { done: 0, failed: invalid };\n const acct = await resolveMacAccount(request.account);\n const raw = await runOsaScript(`tell application \"Microsoft Outlook\"\n${accountLookupSnippet(acct)}\n${rootFolderSnippet(acct, 'drafts', 'draftsFolder')}\n set draftsName to (name of draftsFolder) as string\n set draftIds to id of every message of draftsFolder\n set doneCount to 0\n set out to \"\"\n repeat with theId in ${asIdList(valid)}\n set subj to \"\"\n try\n set theMsg to missing value\n try\n set theMsg to message id theId\n end try\n if theMsg is missing value then error \"no such item in this account's Drafts folder\"\n try\n set subj to (subject of theMsg) as string\n end try\n${ownDraftGuard(request.account, verb)}\n ${verb} theMsg\n set doneCount to doneCount + 1\n on error errText\n set out to out & ${ITEM_FAILURE_ROW}\n end try\n end repeat\n return ${asRow(['(doneCount as string)'])} & out\nend tell`, 'scan');\n const { summary, failed } = splitBatch(raw);\n return { done: intField(summary, 0), failed: [...invalid, ...failed] };\n}\n\n/** Send the named drafts. */\nexport async function sendDrafts(request: EntryIdsRequest): Promise<SendDraftsResult> {\n const { done, failed } = await actOnDrafts(request, 'send');\n return { sent: done, failed };\n}\n\n/**\n * Delete the named drafts. `delete` files an item in Deleted Items, so a\n * mistaken call stays recoverable.\n */\nexport async function deleteOutlookDrafts(request: EntryIdsRequest): Promise<DeleteDraftsResult> {\n const { done, failed } = await actOnDrafts(request, 'delete');\n return { deleted: done, failed };\n}\n\n/**\n * Send every draft belonging to the account. The ids are snapshotted before the\n * first send: sending moves an item out of Drafts, and mutating the collection\n * mid-enumeration would skip every other one. A draft in the folder that is set\n * to send from another account is not this account's, so it is skipped, as on\n * Windows, rather than sent from that account.\n */\nexport async function sendAllDrafts(request: AccountRequest): Promise<SendDraftsResult> {\n const acct = await resolveMacAccount(request.account);\n const raw = await runOsaScript(`tell application \"Microsoft Outlook\"\n${accountLookupSnippet(acct)}\n${rootFolderSnippet(acct, 'drafts', 'draftsFolder')}\n set wanted to id of every message of draftsFolder\n set sentCount to 0\n set out to \"\"\n repeat with theId in wanted\n set subj to \"\"\n try\n set theMsg to missing value\n try\n set theMsg to message id theId\n end try\n if theMsg is missing value then error \"draft is no longer in the Drafts folder\"\n${boundElsewhereSnippet(request.account, ' ')}\n if not boundElsewhere then\n try\n set subj to (subject of theMsg) as string\n end try\n send theMsg\n set sentCount to sentCount + 1\n end if\n on error errText\n set out to out & ${ITEM_FAILURE_ROW}\n end try\n end repeat\n return ${asRow(['(sentCount as string)'])} & out\nend tell`, 'scan');\n const { summary, failed } = splitBatch(raw);\n return { sent: intField(summary, 0), failed };\n}\n"]}
1
+ {"version":3,"file":"drafts.js","sourceRoot":"","sources":["../../src/mac/drafts.ts"],"names":[],"mappings":";;AAqCA,8CAoEC;AAoED,gCAGC;AAMD,kDAGC;AASD,sCA6BC;AA/ND,kFAAkF;AAClF,EAAE;AACF,wEAAwE;AACxE,2EAA2E;AAC3E,0EAA0E;AAC1E,6EAA6E;AAC7E,+BAA+B;AAC/B,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,uDAAuD;AACvD,+BAYe;AACf,uCAQmB;AAInB,sFAAsF;AAC/E,KAAK,UAAU,iBAAiB,CAAC,OAA0B;IAC9D,MAAM,IAAI,GAAG,MAAM,IAAA,2BAAiB,EAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACtD,MAAM,GAAG,GAAG,MAAM,IAAA,kBAAY,EAAC;EACjC,IAAA,8BAAoB,EAAC,IAAI,CAAC;EAC1B,IAAA,2BAAiB,EAAC,IAAI,EAAE,QAAQ,EAAE,cAAc,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2BjD,IAAA,yBAAe,EAAC,UAAU,EAAE,OAAO,CAAC,YAAY,CAAC;;;;;;;;;EASjD,qBAAqB,CAAC,OAAO,CAAC,OAAO,EAAE,UAAU,CAAC;sDACE,IAAA,WAAK,EAAC,CAAC,0BAA0B,EAAE,MAAM,EAAE,4BAA4B,EAAE,0BAA0B,EAAE,UAAU,EAAE,sBAAsB,EAAE,OAAO,CAAC,CAAC;;;SAG/L,EAAE,UAAU,CAAC,CAAC;IAEnB,MAAM,UAAU,GAAG,IAAA,uBAAa,EAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;IAChE,MAAM,MAAM,GAAG,IAAA,kBAAY,EAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;QAC1C,MAAM,KAAK,GAAG,IAAA,iBAAW,EAAC,MAAM,CAAC,CAAC;QAClC,OAAO;YACH,OAAO,EAAE,IAAA,WAAK,EAAC,KAAK,EAAE,CAAC,CAAC;YACxB,OAAO,EAAE,IAAA,WAAK,EAAC,KAAK,EAAE,CAAC,CAAC;YACxB,EAAE,EAAE,IAAA,WAAK,EAAC,KAAK,EAAE,CAAC,CAAC;YACnB,QAAQ,EAAE,IAAA,eAAS,EAAC,IAAA,WAAK,EAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YACpC,WAAW,EAAE,IAAA,WAAK,EAAC,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE;YACxD,cAAc,EAAE,IAAA,cAAQ,EAAC,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC;YACtC,YAAY,EAAE,IAAA,WAAK,EAAC,KAAK,EAAE,CAAC,CAAC;YAC7B,UAAU;SACb,CAAC;IACN,CAAC,CAAC,CAAC;IACH,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC;IACpE,OAAO;QACH,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,cAAc,EAAE,CAAC,UAAU,CAAC;QAC5B,KAAK,EAAE,MAAM,CAAC,MAAM;QACpB,SAAS,EAAE,MAAM,CAAC,MAAM,GAAG,OAAO,CAAC,KAAK;QACxC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC;KACzC,CAAC;AACN,CAAC;AAED;;;;;GAKG;AACH,SAAS,qBAAqB,CAAC,YAAoB,EAAE,MAAc;IAC/D,OAAO,GAAG,MAAM;EAClB,MAAM;EACN,MAAM;EACN,MAAM;EACN,MAAM,+DAA+D,IAAA,cAAQ,EAAC,YAAY,CAAC,GAAG,CAAC;AACjG,CAAC;AAED;;;;GAIG;AACH,SAAS,aAAa,CAAC,YAAoB,EAAE,IAAuB;IAChE,OAAO;;;;8JAImJ,IAAI;EAChK,qBAAqB,CAAC,YAAY,EAAE,cAAc,CAAC;6FACwC,IAAI,GAAG,CAAC;AACrG,CAAC;AAED,oFAAoF;AACpF,KAAK,UAAU,WAAW,CACtB,OAAwB,EACxB,IAAuB;IAEvB,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,IAAA,6BAAmB,EAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;IAC5D,MAAM,IAAI,GAAG,MAAM,IAAA,2BAAiB,EAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACtD,MAAM,GAAG,GAAG,MAAM,IAAA,kBAAY,EAAC;EACjC,IAAA,8BAAoB,EAAC,IAAI,CAAC;EAC1B,IAAA,2BAAiB,EAAC,IAAI,EAAE,QAAQ,EAAE,cAAc,CAAC;;;;;2BAKxB,IAAA,cAAQ,EAAC,KAAK,CAAC;;;EAGxC,IAAA,4BAAkB,EAAC,cAAc,CAAC;;;;;EAKlC,aAAa,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC;cACxB,IAAI;;;+BAGa,sBAAgB;;;aAGlC,IAAA,WAAK,EAAC,CAAC,uBAAuB,CAAC,CAAC;SACpC,EAAE,MAAM,CAAC,CAAC;IACf,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAA,gBAAU,EAAC,GAAG,CAAC,CAAC;IAC5C,OAAO,EAAE,IAAI,EAAE,IAAA,cAAQ,EAAC,OAAO,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC;AAC3E,CAAC;AAED,6BAA6B;AACtB,KAAK,UAAU,UAAU,CAAC,OAAwB;IACrD,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC5D,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAClC,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,mBAAmB,CAAC,OAAwB;IAC9D,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,WAAW,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC9D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AACrC,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,aAAa,CAAC,OAAuB;IACvD,MAAM,IAAI,GAAG,MAAM,IAAA,2BAAiB,EAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACtD,MAAM,GAAG,GAAG,MAAM,IAAA,kBAAY,EAAC;EACjC,IAAA,8BAAoB,EAAC,IAAI,CAAC;EAC1B,IAAA,2BAAiB,EAAC,IAAI,EAAE,QAAQ,EAAE,cAAc,CAAC;;;;;;;EAOjD,IAAA,4BAAkB,EAAC,cAAc,CAAC;;EAElC,qBAAqB,CAAC,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC;;;;;;;;;+BASzB,sBAAgB;;;aAGlC,IAAA,WAAK,EAAC,CAAC,uBAAuB,CAAC,CAAC;SACpC,EAAE,MAAM,CAAC,CAAC;IACf,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAA,gBAAU,EAAC,GAAG,CAAC,CAAC;IAC5C,OAAO,EAAE,IAAI,EAAE,IAAA,cAAQ,EAAC,OAAO,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;AAClD,CAAC","sourcesContent":["// Drafts: which ones belong to an account, and listing, sending or deleting them.\n//\n// Where Windows scans two Drafts folders and proves each item's sending\n// account, macOS resolves the one Drafts folder belonging to the account —\n// through the account object, or by the folder's own id for a mailbox the\n// dictionary won't name (see profile.ts) — so a draft found there belongs to\n// the account by construction.\n//\n// What the folder can't vouch for is an item's own binding, which is why the\n// guards compare against the requested address as a literal rather than asking\n// `targetAcct`: there may be no account object to ask.\nimport {\n asIdList,\n asRow,\n asString,\n field,\n intField,\n ITEM_FAILURE_ROW,\n runOsaScript,\n splitBatch,\n splitFields,\n splitList,\n splitRecords,\n} from './run';\nimport {\n accountLookupSnippet,\n bodyTextSnippet,\n macFolderPath,\n messageByIdSnippet,\n partitionMessageIds,\n resolveMacAccount,\n rootFolderSnippet,\n} from './scripts';\nimport type { AccountRequest, EntryIdsRequest, ListDraftsRequest } from '../backend';\nimport type { DeleteDraftsResult, ItemFailure, ListDraftsResult, SendDraftsResult } from '../types';\n\n/** The account's drafts, newest first. Bodies are previewed, never returned whole. */\nexport async function listOutlookDrafts(request: ListDraftsRequest): Promise<ListDraftsResult> {\n const acct = await resolveMacAccount(request.account);\n const raw = await runOsaScript(`tell application \"Microsoft Outlook\"\n${accountLookupSnippet(acct)}\n${rootFolderSnippet(acct, 'drafts', 'draftsFolder')}\n set out to \"\"\n repeat with theMsg in (messages of draftsFolder)\n set subj to \"\"\n try\n set subj to (subject of theMsg) as string\n end try\n set toNames to {}\n set toAddrs to {}\n try\n repeat with r in (every to recipient of theMsg)\n try\n set ea to email address of r\n set oneAddr to \"\"\n set oneName to \"\"\n try\n set oneAddr to (address of ea) as string\n end try\n try\n set oneName to (name of ea) as string\n end try\n if oneName is \"\" then set oneName to oneAddr\n if oneName is not \"\" then set end of toNames to oneName\n if oneAddr is not \"\" then set end of toAddrs to oneAddr\n end try\n end repeat\n end try\n${bodyTextSnippet(' ', request.previewChars)}\n set attCount to 0\n try\n set attCount to count of attachments of theMsg\n end try\n set modAt to \"\"\n try\n set modAt to my isoDate(modification date of theMsg)\n end try\n${boundElsewhereSnippet(request.account, ' ')}\n if not boundElsewhere then set out to out & ${asRow(['(id of theMsg as string)', 'subj', 'my joinList(toNames, \", \")', 'my sanitizeList(toAddrs)', 'bodyText', '(attCount as string)', 'modAt'])}\n end repeat\n return out\nend tell`, 'standard');\n\n const folderPath = macFolderPath(request.account, 'Drafts', []);\n const drafts = splitRecords(raw).map(record => {\n const parts = splitFields(record);\n return {\n entryId: field(parts, 0),\n subject: field(parts, 1),\n to: field(parts, 2),\n toEmails: splitList(field(parts, 3)),\n bodyPreview: field(parts, 4).replace(/\\s+/g, ' ').trim(),\n hasAttachments: intField(parts, 5) > 0,\n lastModified: field(parts, 6),\n folderPath,\n };\n });\n drafts.sort((a, b) => b.lastModified.localeCompare(a.lastModified));\n return {\n account: request.account,\n foldersScanned: [folderPath],\n count: drafts.length,\n truncated: drafts.length > request.limit,\n drafts: drafts.slice(0, request.limit),\n };\n}\n\n/**\n * Set `boundElsewhere` to whether `theMsg`, a message in the account's Drafts\n * folder, is set to send from some OTHER account. An unbound draft there is the\n * account's own. Listing, sending all and the per-id guard all apply this one\n * rule, as Windows' Test-DraftMatches is applied to all three.\n */\nfunction boundElsewhereSnippet(emailAccount: string, indent: string): string {\n return `${indent}set boundTo to \"\"\n${indent}try\n${indent} set boundTo to (email address of (account of theMsg)) as string\n${indent}end try\n${indent}set boundElsewhere to (boundTo is not \"\" and boundTo is not ${asString(emailAccount)})`;\n}\n\n/**\n * The AppleScript that proves `theMsg` is one of this account's drafts before\n * `verb` happens to it: it sits in the account's Drafts folder, and it is bound\n * to this account or to none.\n */\nfunction ownDraftGuard(emailAccount: string, verb: 'send' | 'delete'): string {\n return ` set inFolder to \"\"\n try\n set inFolder to (name of (folder of theMsg)) as string\n end try\n if inFolder is not draftsName or draftIds does not contain (id of theMsg) then error \"item is not in this account's Drafts folder - refusing to ${verb}\"\n${boundElsewhereSnippet(emailAccount, ' ')}\n if boundElsewhere then error \"draft is not bound to this account - refusing to ${verb}\"`;\n}\n\n/** Run `verb` over named drafts of the account, each proved to be its own first. */\nasync function actOnDrafts(\n request: EntryIdsRequest,\n verb: 'send' | 'delete',\n): Promise<{ done: number; failed: ItemFailure[] }> {\n const { valid, invalid } = partitionMessageIds(request.entryIds);\n if (valid.length === 0) return { done: 0, failed: invalid };\n const acct = await resolveMacAccount(request.account);\n const raw = await runOsaScript(`tell application \"Microsoft Outlook\"\n${accountLookupSnippet(acct)}\n${rootFolderSnippet(acct, 'drafts', 'draftsFolder')}\n set draftsName to (name of draftsFolder) as string\n set draftIds to id of every message of draftsFolder\n set doneCount to 0\n set out to \"\"\n repeat with theId in ${asIdList(valid)}\n set subj to \"\"\n try\n${messageByIdSnippet(' ')}\n if theMsg is missing value then error \"no such item in this account's Drafts folder\"\n try\n set subj to (subject of theMsg) as string\n end try\n${ownDraftGuard(request.account, verb)}\n ${verb} theMsg\n set doneCount to doneCount + 1\n on error errText\n set out to out & ${ITEM_FAILURE_ROW}\n end try\n end repeat\n return ${asRow(['(doneCount as string)'])} & out\nend tell`, 'scan');\n const { summary, failed } = splitBatch(raw);\n return { done: intField(summary, 0), failed: [...invalid, ...failed] };\n}\n\n/** Send the named drafts. */\nexport async function sendDrafts(request: EntryIdsRequest): Promise<SendDraftsResult> {\n const { done, failed } = await actOnDrafts(request, 'send');\n return { sent: done, failed };\n}\n\n/**\n * Delete the named drafts. `delete` files an item in Deleted Items, so a\n * mistaken call stays recoverable.\n */\nexport async function deleteOutlookDrafts(request: EntryIdsRequest): Promise<DeleteDraftsResult> {\n const { done, failed } = await actOnDrafts(request, 'delete');\n return { deleted: done, failed };\n}\n\n/**\n * Send every draft belonging to the account. The ids are snapshotted before the\n * first send: sending moves an item out of Drafts, and mutating the collection\n * mid-enumeration would skip every other one. A draft in the folder that is set\n * to send from another account is not this account's, so it is skipped, as on\n * Windows, rather than sent from that account.\n */\nexport async function sendAllDrafts(request: AccountRequest): Promise<SendDraftsResult> {\n const acct = await resolveMacAccount(request.account);\n const raw = await runOsaScript(`tell application \"Microsoft Outlook\"\n${accountLookupSnippet(acct)}\n${rootFolderSnippet(acct, 'drafts', 'draftsFolder')}\n set wanted to id of every message of draftsFolder\n set sentCount to 0\n set out to \"\"\n repeat with theId in wanted\n set subj to \"\"\n try\n${messageByIdSnippet(' ')}\n if theMsg is missing value then error \"draft is no longer in the Drafts folder\"\n${boundElsewhereSnippet(request.account, ' ')}\n if not boundElsewhere then\n try\n set subj to (subject of theMsg) as string\n end try\n send theMsg\n set sentCount to sentCount + 1\n end if\n on error errText\n set out to out & ${ITEM_FAILURE_ROW}\n end try\n end repeat\n return ${asRow(['(sentCount as string)'])} & out\nend tell`, 'scan');\n const { summary, failed } = splitBatch(raw);\n return { sent: intField(summary, 0), failed };\n}\n"]}
@@ -15,5 +15,10 @@ export declare function listInboxFolders(request: ListFoldersRequest): Promise<I
15
15
  * one real move cover for silent failures, and give no per-item results when it
16
16
  * tripped. A message already in the destination counts as moved without being
17
17
  * moved, as Windows' Move() to its own folder does.
18
+ *
19
+ * Only the account's own mail is moved. A message id resolves against the whole
20
+ * application, so each one is first shown to sit in this account — as Windows
21
+ * resolves every id in the account's store — and one that doesn't fails as its
22
+ * own item rather than being filed out of another mailbox.
18
23
  */
19
24
  export declare function moveOutlookEmails(request: MoveRequest): Promise<MoveEmailsResult>;
@@ -61,6 +61,11 @@ return my walkFolders(inb, 1, ${(0, run_1.asInt)(request.maxDepth)}, "")`, 'stan
61
61
  * one real move cover for silent failures, and give no per-item results when it
62
62
  * tripped. A message already in the destination counts as moved without being
63
63
  * moved, as Windows' Move() to its own folder does.
64
+ *
65
+ * Only the account's own mail is moved. A message id resolves against the whole
66
+ * application, so each one is first shown to sit in this account — as Windows
67
+ * resolves every id in the account's store — and one that doesn't fails as its
68
+ * own item rather than being filed out of another mailbox.
64
69
  */
65
70
  async function moveOutlookEmails(request) {
66
71
  const folderPath = (0, scripts_1.macFolderPath)(request.account, request.folder.rootLabel, request.folder.segments);
@@ -70,6 +75,7 @@ async function moveOutlookEmails(request) {
70
75
  const acct = await (0, scripts_1.resolveMacAccount)(request.account);
71
76
  const raw = await (0, run_1.runOsaScript)(`tell application "Microsoft Outlook"
72
77
  ${(0, scripts_1.accountLookupSnippet)(acct)}
78
+ ${(0, scripts_1.accountRootIdSnippet)(acct)}
73
79
  ${(0, scripts_1.mailScopeSnippet)(acct, request.folder, request.folderLabel, request.createIfMissing)}
74
80
  set scopeId to id of scopeFolder
75
81
  set movedCount to 0
@@ -77,17 +83,20 @@ ${(0, scripts_1.mailScopeSnippet)(acct, request.folder, request.folderLabel, req
77
83
  repeat with theId in ${(0, run_1.asIdList)(valid)}
78
84
  set subj to ""
79
85
  try
80
- set theMsg to missing value
81
- try
82
- set theMsg to message id theId
83
- end try
86
+ ${(0, scripts_1.messageByIdSnippet)(' ')}
84
87
  if theMsg is missing value then error "no message with that id in this Outlook"
85
88
  try
86
89
  set subj to (subject of theMsg) as string
87
90
  end try
91
+ set theFolder to missing value
92
+ try
93
+ set theFolder to folder of theMsg
94
+ end try
95
+ ${(0, scripts_1.folderChainSnippet)(' ')}
96
+ ${(0, scripts_1.ownMailGuardSnippet)(request.account, 'move', ' ')}
88
97
  set fromId to missing value
89
98
  try
90
- set fromId to id of (folder of theMsg)
99
+ set fromId to id of theFolder
91
100
  end try
92
101
  if fromId is not scopeId then
93
102
  set countBefore to count of messages of scopeFolder
@@ -1 +1 @@
1
- {"version":3,"file":"folders.js","sourceRoot":"","sources":["../../src/mac/folders.ts"],"names":[],"mappings":";;AAgCA,4CAyCC;AAYD,8CA6CC;AAlID,gEAAgE;AAChE,+BAce;AACf,uCAOmB;AAInB;;;;GAIG;AACI,KAAK,UAAU,gBAAgB,CAAC,OAA2B;IAC9D,MAAM,IAAI,GAAG,MAAM,IAAA,2BAAiB,EAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACtD,MAAM,GAAG,GAAG,MAAM,IAAA,kBAAY,EAAC;;;;;;;;;;;;;;;;;2BAiBR,IAAA,WAAK,EAAC,CAAC,IAAI,EAAE,UAAU,EAAE,iBAAiB,EAAE,mBAAmB,CAAC,CAAC;;kFAEV,iBAAW;;;;;;;EAO3F,IAAA,8BAAoB,EAAC,IAAI,CAAC;EAC1B,IAAA,2BAAiB,EAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC;;gCAET,IAAA,WAAK,EAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IACxE,OAAO,IAAA,kBAAY,EAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;QAClC,MAAM,KAAK,GAAG,IAAA,iBAAW,EAAC,MAAM,CAAC,CAAC;QAClC,OAAO;YACH,IAAI,EAAE,IAAA,WAAK,EAAC,KAAK,EAAE,CAAC,CAAC;YACrB,UAAU,EAAE,IAAA,uBAAa,EAAC,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,IAAA,eAAS,EAAC,IAAA,WAAK,EAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;YAC/E,SAAS,EAAE,IAAA,cAAQ,EAAC,KAAK,EAAE,CAAC,CAAC;YAC7B,KAAK,EAAE,IAAA,cAAQ,EAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;SAC/B,CAAC;IACN,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;;;;;;;GASG;AACI,KAAK,UAAU,iBAAiB,CAAC,OAAoB;IACxD,MAAM,UAAU,GAAG,IAAA,uBAAa,EAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACrG,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,IAAA,6BAAmB,EAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;IAC/F,MAAM,IAAI,GAAG,MAAM,IAAA,2BAAiB,EAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACtD,MAAM,GAAG,GAAG,MAAM,IAAA,kBAAY,EAAC;EACjC,IAAA,8BAAoB,EAAC,IAAI,CAAC;EAC1B,IAAA,0BAAgB,EAAC,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,eAAe,CAAC;;;;2BAI3D,IAAA,cAAQ,EAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;+BAsBX,sBAAgB;;;aAGlC,IAAA,WAAK,EAAC,CAAC,wBAAwB,EAAE,0BAA0B,CAAC,CAAC;SACjE,EAAE,MAAM,CAAC,CAAC;IACf,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAA,gBAAU,EAAC,GAAG,CAAC,CAAC;IAC5C,OAAO;QACH,UAAU;QACV,aAAa,EAAE,IAAA,eAAS,EAAC,OAAO,EAAE,CAAC,CAAC;QACpC,KAAK,EAAE,IAAA,cAAQ,EAAC,OAAO,EAAE,CAAC,CAAC;QAC3B,MAAM,EAAE,CAAC,GAAG,OAAO,EAAE,GAAG,MAAM,CAAC;KAClC,CAAC;AACN,CAAC","sourcesContent":["// Listing the folders under an Inbox, and filing mail into one.\nimport {\n AS_LIST_SEP,\n asIdList,\n asInt,\n asRow,\n boolField,\n field,\n intField,\n ITEM_FAILURE_ROW,\n runOsaScript,\n splitBatch,\n splitFields,\n splitList,\n splitRecords,\n} from './run';\nimport {\n accountLookupSnippet,\n macFolderPath,\n mailScopeSnippet,\n partitionMessageIds,\n resolveMacAccount,\n rootFolderSnippet,\n} from './scripts';\nimport type { ListFoldersRequest, MoveRequest } from '../backend';\nimport type { InboxFolderInfo, MoveEmailsResult } from '../types';\n\n/**\n * The folders under an account's Inbox with their message counts. Path segments\n * travel joined by the LIST separator and are assembled into a folder path in\n * TypeScript, rather than pushed through two layers of backslash escaping.\n */\nexport async function listInboxFolders(request: ListFoldersRequest): Promise<InboxFolderInfo[]> {\n const acct = await resolveMacAccount(request.account);\n const raw = await runOsaScript(`on walkFolders(theFolder, level, maxLevel, prefix)\n set out to \"\"\n tell application \"Microsoft Outlook\"\n set subs to mail folders of theFolder\n end tell\n repeat with f in subs\n set nm to \"\"\n set cnt to 0\n tell application \"Microsoft Outlook\"\n try\n set nm to (name of f) as string\n end try\n try\n set cnt to (count of messages of f)\n end try\n end tell\n set thisPath to prefix & nm\n set out to out & ${asRow(['nm', 'thisPath', '(cnt as string)', '(level as string)'])}\n if level < maxLevel then\n set out to out & (my walkFolders(f, level + 1, maxLevel, thisPath & ${AS_LIST_SEP}))\n end if\n end repeat\n return out\nend walkFolders\n\ntell application \"Microsoft Outlook\"\n${accountLookupSnippet(acct)}\n${rootFolderSnippet(acct, 'inbox', 'inb')}\nend tell\nreturn my walkFolders(inb, 1, ${asInt(request.maxDepth)}, \"\")`, 'standard');\n return splitRecords(raw).map(record => {\n const parts = splitFields(record);\n return {\n name: field(parts, 0),\n folderPath: macFolderPath(request.account, 'Inbox', splitList(field(parts, 1))),\n itemCount: intField(parts, 2),\n depth: intField(parts, 3, 1),\n };\n });\n}\n\n/**\n * File emails into a folder of the account. Moving rewrites each message's id.\n *\n * Outlook for Mac's `move` reports success even where it silently does nothing,\n * so each move is confirmed by the destination growing, and one that didn't\n * fails as its own item. A count compared once around the whole batch would let\n * one real move cover for silent failures, and give no per-item results when it\n * tripped. A message already in the destination counts as moved without being\n * moved, as Windows' Move() to its own folder does.\n */\nexport async function moveOutlookEmails(request: MoveRequest): Promise<MoveEmailsResult> {\n const folderPath = macFolderPath(request.account, request.folder.rootLabel, request.folder.segments);\n const { valid, invalid } = partitionMessageIds(request.entryIds);\n if (valid.length === 0) return { folderPath, folderCreated: false, moved: 0, failed: invalid };\n const acct = await resolveMacAccount(request.account);\n const raw = await runOsaScript(`tell application \"Microsoft Outlook\"\n${accountLookupSnippet(acct)}\n${mailScopeSnippet(acct, request.folder, request.folderLabel, request.createIfMissing)}\n set scopeId to id of scopeFolder\n set movedCount to 0\n set out to \"\"\n repeat with theId in ${asIdList(valid)}\n set subj to \"\"\n try\n set theMsg to missing value\n try\n set theMsg to message id theId\n end try\n if theMsg is missing value then error \"no message with that id in this Outlook\"\n try\n set subj to (subject of theMsg) as string\n end try\n set fromId to missing value\n try\n set fromId to id of (folder of theMsg)\n end try\n if fromId is not scopeId then\n set countBefore to count of messages of scopeFolder\n move theMsg to scopeFolder\n if (count of messages of scopeFolder) is not greater than countBefore then error \"Outlook reported the move but '\" & (name of scopeFolder) & \"' did not grow - not filed\"\n end if\n set movedCount to movedCount + 1\n on error errText\n set out to out & ${ITEM_FAILURE_ROW}\n end try\n end repeat\n return ${asRow(['(movedCount as string)', '(scopeCreated as string)'])} & out\nend tell`, 'scan');\n const { summary, failed } = splitBatch(raw);\n return {\n folderPath,\n folderCreated: boolField(summary, 1),\n moved: intField(summary, 0),\n failed: [...invalid, ...failed],\n };\n}\n"]}
1
+ {"version":3,"file":"folders.js","sourceRoot":"","sources":["../../src/mac/folders.ts"],"names":[],"mappings":"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","sourcesContent":["// Listing the folders under an Inbox, and filing mail into one.\nimport {\n AS_LIST_SEP,\n asIdList,\n asInt,\n asRow,\n boolField,\n field,\n intField,\n ITEM_FAILURE_ROW,\n runOsaScript,\n splitBatch,\n splitFields,\n splitList,\n splitRecords,\n} from './run';\nimport {\n accountLookupSnippet,\n accountRootIdSnippet,\n folderChainSnippet,\n macFolderPath,\n mailScopeSnippet,\n messageByIdSnippet,\n ownMailGuardSnippet,\n partitionMessageIds,\n resolveMacAccount,\n rootFolderSnippet,\n} from './scripts';\nimport type { ListFoldersRequest, MoveRequest } from '../backend';\nimport type { InboxFolderInfo, MoveEmailsResult } from '../types';\n\n/**\n * The folders under an account's Inbox with their message counts. Path segments\n * travel joined by the LIST separator and are assembled into a folder path in\n * TypeScript, rather than pushed through two layers of backslash escaping.\n */\nexport async function listInboxFolders(request: ListFoldersRequest): Promise<InboxFolderInfo[]> {\n const acct = await resolveMacAccount(request.account);\n const raw = await runOsaScript(`on walkFolders(theFolder, level, maxLevel, prefix)\n set out to \"\"\n tell application \"Microsoft Outlook\"\n set subs to mail folders of theFolder\n end tell\n repeat with f in subs\n set nm to \"\"\n set cnt to 0\n tell application \"Microsoft Outlook\"\n try\n set nm to (name of f) as string\n end try\n try\n set cnt to (count of messages of f)\n end try\n end tell\n set thisPath to prefix & nm\n set out to out & ${asRow(['nm', 'thisPath', '(cnt as string)', '(level as string)'])}\n if level < maxLevel then\n set out to out & (my walkFolders(f, level + 1, maxLevel, thisPath & ${AS_LIST_SEP}))\n end if\n end repeat\n return out\nend walkFolders\n\ntell application \"Microsoft Outlook\"\n${accountLookupSnippet(acct)}\n${rootFolderSnippet(acct, 'inbox', 'inb')}\nend tell\nreturn my walkFolders(inb, 1, ${asInt(request.maxDepth)}, \"\")`, 'standard');\n return splitRecords(raw).map(record => {\n const parts = splitFields(record);\n return {\n name: field(parts, 0),\n folderPath: macFolderPath(request.account, 'Inbox', splitList(field(parts, 1))),\n itemCount: intField(parts, 2),\n depth: intField(parts, 3, 1),\n };\n });\n}\n\n/**\n * File emails into a folder of the account. Moving rewrites each message's id.\n *\n * Outlook for Mac's `move` reports success even where it silently does nothing,\n * so each move is confirmed by the destination growing, and one that didn't\n * fails as its own item. A count compared once around the whole batch would let\n * one real move cover for silent failures, and give no per-item results when it\n * tripped. A message already in the destination counts as moved without being\n * moved, as Windows' Move() to its own folder does.\n *\n * Only the account's own mail is moved. A message id resolves against the whole\n * application, so each one is first shown to sit in this account — as Windows\n * resolves every id in the account's store — and one that doesn't fails as its\n * own item rather than being filed out of another mailbox.\n */\nexport async function moveOutlookEmails(request: MoveRequest): Promise<MoveEmailsResult> {\n const folderPath = macFolderPath(request.account, request.folder.rootLabel, request.folder.segments);\n const { valid, invalid } = partitionMessageIds(request.entryIds);\n if (valid.length === 0) return { folderPath, folderCreated: false, moved: 0, failed: invalid };\n const acct = await resolveMacAccount(request.account);\n const raw = await runOsaScript(`tell application \"Microsoft Outlook\"\n${accountLookupSnippet(acct)}\n${accountRootIdSnippet(acct)}\n${mailScopeSnippet(acct, request.folder, request.folderLabel, request.createIfMissing)}\n set scopeId to id of scopeFolder\n set movedCount to 0\n set out to \"\"\n repeat with theId in ${asIdList(valid)}\n set subj to \"\"\n try\n${messageByIdSnippet(' ')}\n if theMsg is missing value then error \"no message with that id in this Outlook\"\n try\n set subj to (subject of theMsg) as string\n end try\n set theFolder to missing value\n try\n set theFolder to folder of theMsg\n end try\n${folderChainSnippet(' ')}\n${ownMailGuardSnippet(request.account, 'move', ' ')}\n set fromId to missing value\n try\n set fromId to id of theFolder\n end try\n if fromId is not scopeId then\n set countBefore to count of messages of scopeFolder\n move theMsg to scopeFolder\n if (count of messages of scopeFolder) is not greater than countBefore then error \"Outlook reported the move but '\" & (name of scopeFolder) & \"' did not grow - not filed\"\n end if\n set movedCount to movedCount + 1\n on error errText\n set out to out & ${ITEM_FAILURE_ROW}\n end try\n end repeat\n return ${asRow(['(movedCount as string)', '(scopeCreated as string)'])} & out\nend tell`, 'scan');\n const { summary, failed } = splitBatch(raw);\n return {\n folderPath,\n folderCreated: boolField(summary, 1),\n moved: intField(summary, 0),\n failed: [...invalid, ...failed],\n };\n}\n"]}
@@ -4,6 +4,18 @@ export interface ProfileAccount {
4
4
  /** AppleScript folder id per root term — the keys of `ROOT_SPECIAL_TYPES`. */
5
5
  folderIds: Record<string, number>;
6
6
  }
7
+ /**
8
+ * Why Outlook's profile can't be read, when macOS privacy control is the
9
+ * reason: the sentence to act on, naming the app to grant access to. null when
10
+ * the profile can be read, or simply isn't there — New Outlook, or no Outlook
11
+ * profile on this Mac.
12
+ *
13
+ * Everything else in this module stays silent on purpose, which is right for an
14
+ * enrichment pass and wrong when the pass was the only way to see a mailbox:
15
+ * the caller then hears "no accounts" or "not found" about a mailbox in plain
16
+ * sight in Outlook. The callers that would say that ask here first.
17
+ */
18
+ export declare function profileRefusal(): Promise<string | null>;
7
19
  /**
8
20
  * Every mail account the profile knows, whether or not AppleScript can see it.
9
21
  *
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.profileRefusal = profileRefusal;
6
7
  exports.readProfileAccounts = readProfileAccounts;
7
8
  // Outlook's own profile database, read for the mailboxes the dictionary omits.
8
9
  //
@@ -24,7 +25,9 @@ exports.readProfileAccounts = readProfileAccounts;
24
25
  // folder id wherever the account probe comes up empty.
25
26
  //
26
27
  // Strictly read-only, and never fatal: every failure here leaves the caller with
27
- // exactly what AppleScript could see on its own.
28
+ // exactly what AppleScript could see on its own. When macOS privacy control is
29
+ // what failed, `profileRefusal` says so, for the callers that would otherwise
30
+ // report a mailbox in plain sight as missing.
28
31
  const child_process_1 = require("child_process");
29
32
  const fs_1 = __importDefault(require("fs"));
30
33
  const os_1 = __importDefault(require("os"));
@@ -111,6 +114,77 @@ function query(database, sql) {
111
114
  (0, child_process_1.execFile)('sqlite3', ['-readonly', '-list', '-noheader', '-separator', SEP, database, sql], { timeout: 5000, maxBuffer: 1024 * 1024 }, (error, stdout) => resolve(error ? '' : stdout));
112
115
  });
113
116
  }
117
+ /** The app macOS holds this process's file access against, once asked. */
118
+ let responsible = null;
119
+ /**
120
+ * The app macOS privacy control asks about this process's file access, by name:
121
+ * the outermost app bundle among its ancestors, the one launchd started. A
122
+ * process started from a terminal or an editor is that app's to grant, not its
123
+ * own, so naming this process would send the person to the wrong switch. '' when
124
+ * it can't be told.
125
+ */
126
+ function responsibleApp() {
127
+ responsible ??= new Promise(resolve => {
128
+ (0, child_process_1.execFile)('ps', ['-A', '-o', 'pid=,ppid=,comm='], {
129
+ timeout: 5000,
130
+ maxBuffer: 4 * 1024 * 1024
131
+ }, (error, stdout) => {
132
+ if (error)
133
+ return resolve('');
134
+ const processes = new Map();
135
+ for (const line of stdout.split('\n')) {
136
+ const row = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line);
137
+ if (row)
138
+ processes.set(Number(row[1]), { ppid: Number(row[2]), command: row[3] ?? '' });
139
+ }
140
+ let app = '';
141
+ const climbed = new Set();
142
+ for (let pid = process.pid; pid > 1 && !climbed.has(pid);) {
143
+ climbed.add(pid);
144
+ const entry = processes.get(pid);
145
+ if (!entry)
146
+ break;
147
+ // The first bundle in a path is the outer one: a helper app sits
148
+ // inside its parent's. Each one higher up replaces the last.
149
+ const bundle = /\/([^/]+)\.app(?:\/|$)/.exec(entry.command)?.[1];
150
+ if (bundle)
151
+ app = bundle;
152
+ pid = entry.ppid;
153
+ }
154
+ resolve(app);
155
+ });
156
+ });
157
+ return responsible;
158
+ }
159
+ /**
160
+ * Why Outlook's profile can't be read, when macOS privacy control is the
161
+ * reason: the sentence to act on, naming the app to grant access to. null when
162
+ * the profile can be read, or simply isn't there — New Outlook, or no Outlook
163
+ * profile on this Mac.
164
+ *
165
+ * Everything else in this module stays silent on purpose, which is right for an
166
+ * enrichment pass and wrong when the pass was the only way to see a mailbox:
167
+ * the caller then hears "no accounts" or "not found" about a mailbox in plain
168
+ * sight in Outlook. The callers that would say that ask here first.
169
+ */
170
+ async function profileRefusal() {
171
+ try {
172
+ fs_1.default.readdirSync(PROFILES_DIR);
173
+ return null;
174
+ }
175
+ catch (error) {
176
+ const code = error.code;
177
+ if (code !== 'EPERM' && code !== 'EACCES')
178
+ return null;
179
+ }
180
+ const app = await responsibleApp();
181
+ const who = app || 'the app this runs in (the terminal or editor it was started from, if it was)';
182
+ const it = app || 'that app';
183
+ // Plain ASCII: the sentence also travels inside a generated script's string literal.
184
+ return `macOS is not letting ${who} read Outlook's profile, the only place Outlook for Mac lists `
185
+ + "a Microsoft 365 mailbox it syncs through Microsoft's cloud. "
186
+ + `Turn on ${it} under System Settings > Privacy & Security > Full Disk Access, then quit ${it} and open it again.`;
187
+ }
114
188
  /**
115
189
  * Every mail account the profile knows, whether or not AppleScript can see it.
116
190
  *
@@ -1 +1 @@
1
- {"version":3,"file":"profile.js","sourceRoot":"","sources":["../../src/mac/profile.ts"],"names":[],"mappings":"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","sourcesContent":["// Outlook's own profile database, read for the mailboxes the dictionary omits.\n//\n// `getOutlookAccounts` can only enumerate `exchange accounts`, `imap accounts`\n// and `pop accounts`, and a Microsoft 365 mailbox added by the modern sync\n// engine is none of those: Outlook files it in `AccountsMail` under a 'SHDO'\n// server type, publishes no account object for it, and answers `every account`\n// with -1728 rather than listing it. Even `account of <folder>` and\n// `account of <message>` come back `missing value` inside that mailbox's own\n// tree, so there is nothing to match an address against — the mailbox is\n// invisible to every function in this directory while its mail sits in plain\n// sight in the folder list.\n//\n// The folders themselves are reachable, because `mail folder id N` resolves\n// against the application rather than through an account, and N is exactly the\n// `Record_RecordID` Outlook's profile database gives that folder. So the\n// addresses AppleScript can't report, and the ids of their well-known folders,\n// are read from that database instead, and the generated scripts fall back to a\n// folder id wherever the account probe comes up empty.\n//\n// Strictly read-only, and never fatal: every failure here leaves the caller with\n// exactly what AppleScript could see on its own.\nimport { execFile } from 'child_process';\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\n/** Where Outlook keeps its profiles, and the profile used when there are several. */\nconst PROFILES_DIR = path.join(\n os.homedir(),\n 'Library', 'Group Containers', 'UBF8T346G9.Office', 'Outlook', 'Outlook 15 Profiles',\n);\nconst DEFAULT_PROFILE = 'Main Profile';\n\n/**\n * `Folder_SpecialFolderType` for each root the generated scripts ask for, keyed\n * by the dictionary term `MAC_ROOT_TERMS` resolves.\n *\n * These are Outlook's own numbering, confirmed against a profile's local \"On My\n * Computer\" store, where every one of them appears exactly once: 1 Inbox,\n * 2 Outbox, 8 Sent Items, 9 Deleted Items, 10 Drafts, 12 Junk, 99 the account\n * root. Outbox is left out: a caller can still name it (`MAC_ROOT_TERMS` maps it),\n * but nothing here files mail into one, so it is not worth an id — that request\n * falls back to the account probe like any root the profile has no row for.\n */\nconst ROOT_SPECIAL_TYPES: Readonly<Record<string, number>> = {\n 'inbox': 1,\n 'sent items': 8,\n 'deleted items': 9,\n 'drafts': 10,\n 'junk mail': 12,\n 'root folder': 99,\n};\n\n/** One mail account as the profile database records it. */\nexport interface ProfileAccount {\n emailAccount: string;\n /** AppleScript folder id per root term — the keys of `ROOT_SPECIAL_TYPES`. */\n folderIds: Record<string, number>;\n}\n\n/** Column separator for the query output - a unit separator cannot occur in a row. */\nconst SEP = '\\x1f';\n\n/**\n * Address and well-known folder ids for every mail account in the profile.\n *\n * `Record_AccountUID` is a tagged id — the account's `Record_RecordID` in its low\n * 32 bits, a table tag above — so the join masks rather than assuming the tag,\n * and drops UID 0, which is the local \"On My Computer\" store rather than an\n * account.\n */\nconst ACCOUNTS_SQL = `\n SELECT a.Account_EmailAddress, f.Folder_SpecialFolderType, f.Record_RecordID\n FROM AccountsMail a\n JOIN Folders f ON (f.Record_AccountUID & 4294967295) = a.Record_RecordID\n WHERE f.Record_AccountUID <> 0\n AND a.Account_EmailAddress IS NOT NULL\n AND a.Account_EmailAddress <> ''\n AND f.Folder_SpecialFolderType IN (${Object.values(ROOT_SPECIAL_TYPES).join(', ')})\n ORDER BY a.Record_RecordID, f.Folder_SpecialFolderType`;\n\n/** The profile whose database to read, or '' when there is none to read. */\nfunction profileDatabase(): string {\n let names: string[];\n try {\n names = fs.readdirSync(PROFILES_DIR);\n } catch {\n return '';\n }\n // Outlook records no \"default profile\" anywhere reachable — not in its\n // preferences, not beside the profiles — so with more than one the newest\n // wins, after the name Outlook itself creates and all but everyone keeps.\n const dbOf = (name: string) => path.join(PROFILES_DIR, name, 'Data', 'Outlook.sqlite');\n // Outlook rewrites these files while it runs, and the whole directory sits\n // behind macOS privacy control, so a database that existed a moment ago need\n // not stat. An unreadable timestamp sorts last rather than throwing: this\n // runs on nearly every macOS operation (resolveMacAccount), and a mailbox\n // AppleScript could have named on its own must not be lost to it.\n const mtimeOf = (db: string) => {\n try {\n return fs.statSync(db).mtimeMs;\n } catch {\n return -Infinity;\n }\n };\n const candidates = names\n .map(dbOf)\n .filter(db => fs.existsSync(db))\n .sort((a, b) => mtimeOf(b) - mtimeOf(a));\n const preferred = dbOf(DEFAULT_PROFILE);\n if (candidates.includes(preferred)) return preferred;\n return candidates[0] ?? '';\n}\n\n/** Run one read-only query, or return '' if anything at all gets in the way. */\nfunction query(database: string, sql: string): Promise<string> {\n return new Promise(resolve => {\n execFile(\n 'sqlite3',\n ['-readonly', '-list', '-noheader', '-separator', SEP, database, sql],\n { timeout: 5000, maxBuffer: 1024 * 1024 },\n (error, stdout) => resolve(error ? '' : stdout),\n );\n });\n}\n\n/**\n * Every mail account the profile knows, whether or not AppleScript can see it.\n *\n * An account whose row is missing a given root simply has no id for it, and the\n * caller keeps whatever the account probe would have done.\n */\nexport async function readProfileAccounts(): Promise<ProfileAccount[]> {\n try {\n return await readAccounts();\n } catch {\n // The module's contract, enforced at its one exit: this is an enrichment\n // pass over what AppleScript already found, so nothing that happens in it\n // is worth failing an operation that would otherwise have worked.\n return [];\n }\n}\n\nasync function readAccounts(): Promise<ProfileAccount[]> {\n const database = profileDatabase();\n if (!database) return [];\n const termOf = new Map(Object.entries(ROOT_SPECIAL_TYPES).map(([term, type]) => [type, term]));\n const byAddress = new Map<string, ProfileAccount>();\n for (const line of (await query(database, ACCOUNTS_SQL)).split('\\n')) {\n const [address, specialType, folderId] = line.split(SEP);\n const term = termOf.get(Number(specialType));\n if (!address || !term || !/^\\d+$/.test(folderId ?? '')) continue;\n const account = byAddress.get(address.toLowerCase())\n ?? { emailAccount: address, folderIds: {} };\n account.folderIds[term] = Number(folderId);\n byAddress.set(address.toLowerCase(), account);\n }\n return [...byAddress.values()];\n}\n"]}
1
+ {"version":3,"file":"profile.js","sourceRoot":"","sources":["../../src/mac/profile.ts"],"names":[],"mappings":"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","sourcesContent":["// Outlook's own profile database, read for the mailboxes the dictionary omits.\n//\n// `getOutlookAccounts` can only enumerate `exchange accounts`, `imap accounts`\n// and `pop accounts`, and a Microsoft 365 mailbox added by the modern sync\n// engine is none of those: Outlook files it in `AccountsMail` under a 'SHDO'\n// server type, publishes no account object for it, and answers `every account`\n// with -1728 rather than listing it. Even `account of <folder>` and\n// `account of <message>` come back `missing value` inside that mailbox's own\n// tree, so there is nothing to match an address against — the mailbox is\n// invisible to every function in this directory while its mail sits in plain\n// sight in the folder list.\n//\n// The folders themselves are reachable, because `mail folder id N` resolves\n// against the application rather than through an account, and N is exactly the\n// `Record_RecordID` Outlook's profile database gives that folder. So the\n// addresses AppleScript can't report, and the ids of their well-known folders,\n// are read from that database instead, and the generated scripts fall back to a\n// folder id wherever the account probe comes up empty.\n//\n// Strictly read-only, and never fatal: every failure here leaves the caller with\n// exactly what AppleScript could see on its own. When macOS privacy control is\n// what failed, `profileRefusal` says so, for the callers that would otherwise\n// report a mailbox in plain sight as missing.\nimport { execFile } from 'child_process';\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\n/** Where Outlook keeps its profiles, and the profile used when there are several. */\nconst PROFILES_DIR = path.join(\n os.homedir(),\n 'Library', 'Group Containers', 'UBF8T346G9.Office', 'Outlook', 'Outlook 15 Profiles',\n);\nconst DEFAULT_PROFILE = 'Main Profile';\n\n/**\n * `Folder_SpecialFolderType` for each root the generated scripts ask for, keyed\n * by the dictionary term `MAC_ROOT_TERMS` resolves.\n *\n * These are Outlook's own numbering, confirmed against a profile's local \"On My\n * Computer\" store, where every one of them appears exactly once: 1 Inbox,\n * 2 Outbox, 8 Sent Items, 9 Deleted Items, 10 Drafts, 12 Junk, 99 the account\n * root. Outbox is left out: a caller can still name it (`MAC_ROOT_TERMS` maps it),\n * but nothing here files mail into one, so it is not worth an id — that request\n * falls back to the account probe like any root the profile has no row for.\n */\nconst ROOT_SPECIAL_TYPES: Readonly<Record<string, number>> = {\n 'inbox': 1,\n 'sent items': 8,\n 'deleted items': 9,\n 'drafts': 10,\n 'junk mail': 12,\n 'root folder': 99,\n};\n\n/** One mail account as the profile database records it. */\nexport interface ProfileAccount {\n emailAccount: string;\n /** AppleScript folder id per root term — the keys of `ROOT_SPECIAL_TYPES`. */\n folderIds: Record<string, number>;\n}\n\n/** Column separator for the query output - a unit separator cannot occur in a row. */\nconst SEP = '\\x1f';\n\n/**\n * Address and well-known folder ids for every mail account in the profile.\n *\n * `Record_AccountUID` is a tagged id — the account's `Record_RecordID` in its low\n * 32 bits, a table tag above — so the join masks rather than assuming the tag,\n * and drops UID 0, which is the local \"On My Computer\" store rather than an\n * account.\n */\nconst ACCOUNTS_SQL = `\n SELECT a.Account_EmailAddress, f.Folder_SpecialFolderType, f.Record_RecordID\n FROM AccountsMail a\n JOIN Folders f ON (f.Record_AccountUID & 4294967295) = a.Record_RecordID\n WHERE f.Record_AccountUID <> 0\n AND a.Account_EmailAddress IS NOT NULL\n AND a.Account_EmailAddress <> ''\n AND f.Folder_SpecialFolderType IN (${Object.values(ROOT_SPECIAL_TYPES).join(', ')})\n ORDER BY a.Record_RecordID, f.Folder_SpecialFolderType`;\n\n/** The profile whose database to read, or '' when there is none to read. */\nfunction profileDatabase(): string {\n let names: string[];\n try {\n names = fs.readdirSync(PROFILES_DIR);\n } catch {\n return '';\n }\n // Outlook records no \"default profile\" anywhere reachable — not in its\n // preferences, not beside the profiles — so with more than one the newest\n // wins, after the name Outlook itself creates and all but everyone keeps.\n const dbOf = (name: string) => path.join(PROFILES_DIR, name, 'Data', 'Outlook.sqlite');\n // Outlook rewrites these files while it runs, and the whole directory sits\n // behind macOS privacy control, so a database that existed a moment ago need\n // not stat. An unreadable timestamp sorts last rather than throwing: this\n // runs on nearly every macOS operation (resolveMacAccount), and a mailbox\n // AppleScript could have named on its own must not be lost to it.\n const mtimeOf = (db: string) => {\n try {\n return fs.statSync(db).mtimeMs;\n } catch {\n return -Infinity;\n }\n };\n const candidates = names\n .map(dbOf)\n .filter(db => fs.existsSync(db))\n .sort((a, b) => mtimeOf(b) - mtimeOf(a));\n const preferred = dbOf(DEFAULT_PROFILE);\n if (candidates.includes(preferred)) return preferred;\n return candidates[0] ?? '';\n}\n\n/** Run one read-only query, or return '' if anything at all gets in the way. */\nfunction query(database: string, sql: string): Promise<string> {\n return new Promise(resolve => {\n execFile(\n 'sqlite3',\n ['-readonly', '-list', '-noheader', '-separator', SEP, database, sql],\n { timeout: 5000, maxBuffer: 1024 * 1024 },\n (error, stdout) => resolve(error ? '' : stdout),\n );\n });\n}\n\n/** The app macOS holds this process's file access against, once asked. */\nlet responsible: Promise<string> | null = null;\n\n/**\n * The app macOS privacy control asks about this process's file access, by name:\n * the outermost app bundle among its ancestors, the one launchd started. A\n * process started from a terminal or an editor is that app's to grant, not its\n * own, so naming this process would send the person to the wrong switch. '' when\n * it can't be told.\n */\nfunction responsibleApp(): Promise<string> {\n responsible ??= new Promise(resolve => {\n execFile('ps', ['-A', '-o', 'pid=,ppid=,comm='], {\n timeout: 5000,\n maxBuffer: 4 * 1024 * 1024\n }, (error, stdout) => {\n if (error) return resolve('');\n const processes = new Map<number, { ppid: number; command: string }>();\n for (const line of stdout.split('\\n')) {\n const row = /^\\s*(\\d+)\\s+(\\d+)\\s+(.*)$/.exec(line);\n if (row) processes.set(Number(row[1]), { ppid: Number(row[2]), command: row[3] ?? '' });\n }\n let app = '';\n const climbed = new Set<number>();\n for (let pid = process.pid; pid > 1 && !climbed.has(pid);) {\n climbed.add(pid);\n const entry = processes.get(pid);\n if (!entry) break;\n // The first bundle in a path is the outer one: a helper app sits\n // inside its parent's. Each one higher up replaces the last.\n const bundle = /\\/([^/]+)\\.app(?:\\/|$)/.exec(entry.command)?.[1];\n if (bundle) app = bundle;\n pid = entry.ppid;\n }\n resolve(app);\n });\n });\n return responsible;\n}\n\n/**\n * Why Outlook's profile can't be read, when macOS privacy control is the\n * reason: the sentence to act on, naming the app to grant access to. null when\n * the profile can be read, or simply isn't there — New Outlook, or no Outlook\n * profile on this Mac.\n *\n * Everything else in this module stays silent on purpose, which is right for an\n * enrichment pass and wrong when the pass was the only way to see a mailbox:\n * the caller then hears \"no accounts\" or \"not found\" about a mailbox in plain\n * sight in Outlook. The callers that would say that ask here first.\n */\nexport async function profileRefusal(): Promise<string | null> {\n try {\n fs.readdirSync(PROFILES_DIR);\n return null;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code !== 'EPERM' && code !== 'EACCES') return null;\n }\n const app = await responsibleApp();\n const who = app || 'the app this runs in (the terminal or editor it was started from, if it was)';\n const it = app || 'that app';\n // Plain ASCII: the sentence also travels inside a generated script's string literal.\n return `macOS is not letting ${who} read Outlook's profile, the only place Outlook for Mac lists `\n + \"a Microsoft 365 mailbox it syncs through Microsoft's cloud. \"\n + `Turn on ${it} under System Settings > Privacy & Security > Full Disk Access, then quit ${it} and open it again.`;\n}\n\n/**\n * Every mail account the profile knows, whether or not AppleScript can see it.\n *\n * An account whose row is missing a given root simply has no id for it, and the\n * caller keeps whatever the account probe would have done.\n */\nexport async function readProfileAccounts(): Promise<ProfileAccount[]> {\n try {\n return await readAccounts();\n } catch {\n // The module's contract, enforced at its one exit: this is an enrichment\n // pass over what AppleScript already found, so nothing that happens in it\n // is worth failing an operation that would otherwise have worked.\n return [];\n }\n}\n\nasync function readAccounts(): Promise<ProfileAccount[]> {\n const database = profileDatabase();\n if (!database) return [];\n const termOf = new Map(Object.entries(ROOT_SPECIAL_TYPES).map(([term, type]) => [type, term]));\n const byAddress = new Map<string, ProfileAccount>();\n for (const line of (await query(database, ACCOUNTS_SQL)).split('\\n')) {\n const [address, specialType, folderId] = line.split(SEP);\n const term = termOf.get(Number(specialType));\n if (!address || !term || !/^\\d+$/.test(folderId ?? '')) continue;\n const account = byAddress.get(address.toLowerCase())\n ?? { emailAccount: address, folderIds: {} };\n account.folderIds[term] = Number(folderId);\n byAddress.set(address.toLowerCase(), account);\n }\n return [...byAddress.values()];\n}\n"]}