@dawnlit/outlook-bridge 2.0.0 → 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.
- package/README.md +6 -4
- package/dist/backend.d.ts +2 -2
- package/dist/backend.js.map +1 -1
- package/dist/bridge.js +3 -3
- package/dist/bridge.js.map +1 -1
- package/dist/errors.d.ts +0 -2
- package/dist/errors.js +1 -6
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/mac/accounts.js.map +1 -1
- package/dist/mac/attachments.js +1 -1
- package/dist/mac/attachments.js.map +1 -1
- package/dist/mac/bounces.js +8 -12
- package/dist/mac/bounces.js.map +1 -1
- package/dist/mac/cleanup.js +4 -41
- package/dist/mac/cleanup.js.map +1 -1
- package/dist/mac/drafts.d.ts +3 -1
- package/dist/mac/drafts.js +34 -33
- package/dist/mac/drafts.js.map +1 -1
- package/dist/mac/folders.d.ts +5 -3
- package/dist/mac/folders.js +18 -19
- package/dist/mac/folders.js.map +1 -1
- package/dist/mac/profile.js.map +1 -1
- package/dist/mac/read.d.ts +4 -0
- package/dist/mac/read.js +54 -16
- package/dist/mac/read.js.map +1 -1
- package/dist/mac/run.d.ts +19 -2
- package/dist/mac/run.js +42 -2
- package/dist/mac/run.js.map +1 -1
- package/dist/mac/scripts.d.ts +21 -14
- package/dist/mac/scripts.js +52 -21
- package/dist/mac/scripts.js.map +1 -1
- package/dist/mac/send.d.ts +3 -3
- package/dist/mac/send.js +44 -12
- package/dist/mac/send.js.map +1 -1
- package/dist/mac/templates.js +15 -18
- package/dist/mac/templates.js.map +1 -1
- package/dist/mail.d.ts +5 -3
- package/dist/mail.js +19 -6
- package/dist/mail.js.map +1 -1
- package/dist/runtime.js.map +1 -1
- package/dist/shared/args.js.map +1 -1
- package/dist/shared/attachmentMatch.d.ts +0 -6
- package/dist/shared/attachmentMatch.js +0 -1
- package/dist/shared/attachmentMatch.js.map +1 -1
- package/dist/shared/exec.js.map +1 -1
- package/dist/shared/json.d.ts +11 -6
- package/dist/shared/json.js +12 -7
- package/dist/shared/json.js.map +1 -1
- package/dist/shared/locate.d.ts +0 -8
- package/dist/shared/locate.js +0 -1
- package/dist/shared/locate.js.map +1 -1
- package/dist/shared/replyBody.d.ts +0 -15
- package/dist/shared/replyBody.js +0 -2
- package/dist/shared/replyBody.js.map +1 -1
- package/dist/shared/templateLimits.d.ts +14 -0
- package/dist/shared/templateLimits.js +21 -0
- package/dist/shared/templateLimits.js.map +1 -0
- package/dist/templateBody.d.ts +6 -0
- package/dist/templateBody.js +8 -1
- package/dist/templateBody.js.map +1 -1
- package/dist/types.d.ts +23 -3
- package/dist/types.js.map +1 -1
- package/dist/windows/bounces.js +4 -4
- package/dist/windows/bounces.js.map +1 -1
- package/dist/windows/cleanup.d.ts +5 -1
- package/dist/windows/cleanup.js +16 -11
- package/dist/windows/cleanup.js.map +1 -1
- package/dist/windows/drafts.d.ts +4 -4
- package/dist/windows/drafts.js +20 -31
- package/dist/windows/drafts.js.map +1 -1
- package/dist/windows/folders.js +1 -1
- package/dist/windows/folders.js.map +1 -1
- package/dist/windows/read.js +6 -2
- package/dist/windows/read.js.map +1 -1
- package/dist/windows/run.d.ts +0 -2
- package/dist/windows/run.js +1 -2
- package/dist/windows/run.js.map +1 -1
- package/dist/windows/scripts.d.ts +4 -4
- package/dist/windows/scripts.js +10 -10
- package/dist/windows/scripts.js.map +1 -1
- package/dist/windows/send.d.ts +8 -3
- package/dist/windows/send.js +10 -2
- package/dist/windows/send.js.map +1 -1
- package/dist/windows/templates.js +9 -15
- package/dist/windows/templates.js.map +1 -1
- package/package.json +2 -2
package/dist/mac/profile.js.map
CHANGED
|
@@ -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.\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"]}
|
package/dist/mac/read.d.ts
CHANGED
|
@@ -22,6 +22,10 @@ export declare function searchInboxByFilter(request: SearchRequest): Promise<Inb
|
|
|
22
22
|
/**
|
|
23
23
|
* The email currently selected — or open — in Outlook. `current messages`
|
|
24
24
|
* covers both cases the Windows reader handles separately.
|
|
25
|
+
*
|
|
26
|
+
* Where it sits comes back as a second record, so the detail row keeps its body
|
|
27
|
+
* last. A mailbox with no account object answers `account of` with missing
|
|
28
|
+
* value; its root folder's id is then looked up in the profile database.
|
|
25
29
|
*/
|
|
26
30
|
export declare function readSelectedEmail(): Promise<SelectedEmail>;
|
|
27
31
|
/** Open an email in Outlook and bring Outlook forward. */
|
package/dist/mac/read.js
CHANGED
|
@@ -14,6 +14,7 @@ exports.openOutlookEmail = openOutlookEmail;
|
|
|
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
19
|
const errors_1 = require("../errors");
|
|
19
20
|
const mail_1 = require("../mail");
|
|
@@ -81,13 +82,13 @@ end tell`, 'standard'))
|
|
|
81
82
|
try
|
|
82
83
|
set subj to (subject of theMsg) as string
|
|
83
84
|
end try
|
|
84
|
-
${outgoing ? (0, scripts_1.firstRecipientSnippet)('
|
|
85
|
+
${outgoing ? (0, scripts_1.firstRecipientSnippet)(' ') : (0, scripts_1.senderSnippet)(' ')}
|
|
85
86
|
${preview}
|
|
86
87
|
set attNames to {}
|
|
87
88
|
try
|
|
88
89
|
set attNames to name of every attachment of theMsg
|
|
89
90
|
end try
|
|
90
|
-
set out to out & ${(0, run_1.asRow)(['(id of theMsg as string)', 'subj', 'sndName', 'sndAddr', 'bodyText', 'my
|
|
91
|
+
set out to out & ${(0, run_1.asRow)(['(id of theMsg as string)', 'subj', 'sndName', 'sndAddr', 'bodyText', 'my sanitizePositionalList(attNames)'])}
|
|
91
92
|
end if
|
|
92
93
|
end repeat
|
|
93
94
|
return out
|
|
@@ -96,7 +97,7 @@ end tell`, 'standard');
|
|
|
96
97
|
return (0, run_1.splitRecords)(raw).map(record => {
|
|
97
98
|
const parts = (0, run_1.splitFields)(record);
|
|
98
99
|
const id = (0, run_1.field)(parts, 0);
|
|
99
|
-
const attachmentNames = (0, run_1.
|
|
100
|
+
const attachmentNames = (0, run_1.splitPositionalList)((0, run_1.field)(parts, 5));
|
|
100
101
|
return {
|
|
101
102
|
entryId: id,
|
|
102
103
|
storeId: '',
|
|
@@ -129,7 +130,7 @@ end tell`, 'quick'));
|
|
|
129
130
|
senderEmail: (0, run_1.field)(parts, scripts_1.MessageDetail.senderEmail),
|
|
130
131
|
receivedTime: (0, run_1.field)(parts, scripts_1.MessageDetail.receivedTime),
|
|
131
132
|
body: (0, run_1.field)(parts, scripts_1.MessageDetail.body),
|
|
132
|
-
attachmentNames: (0, run_1.
|
|
133
|
+
attachmentNames: (0, run_1.splitPositionalList)((0, run_1.field)(parts, scripts_1.MessageDetail.attachmentNames)),
|
|
133
134
|
};
|
|
134
135
|
}
|
|
135
136
|
/**
|
|
@@ -226,8 +227,13 @@ set cutoff to (current date) - (${(0, run_1.asInt)(request.daysBack)} * days)
|
|
|
226
227
|
return my scanFolder(rootInbox, rootName, cutoff, skipIds, skipNames, onlyNames, ${rootInScope})`, 'scan'));
|
|
227
228
|
const subjectFiltered = !!(request.subjectLike || request.subjectPattern || request.excludeReplies);
|
|
228
229
|
const likeRe = request.subjectLike ? new RegExp((0, mail_1.subjectGlobSource)(request.subjectLike), 'i') : null;
|
|
229
|
-
// PowerShell's -match is case-insensitive, so
|
|
230
|
-
|
|
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, ''))
|
|
236
|
+
: null;
|
|
231
237
|
const candidates = indexed
|
|
232
238
|
.map(record => {
|
|
233
239
|
const parts = (0, run_1.splitFields)(record);
|
|
@@ -259,7 +265,6 @@ return my scanFolder(rootInbox, rootName, cutoff, skipIds, skipNames, onlyNames,
|
|
|
259
265
|
set bodyText to (plain text content of theMsg) as string
|
|
260
266
|
end try`
|
|
261
267
|
: ' set bodyText to ""';
|
|
262
|
-
const details = new Map();
|
|
263
268
|
const raw = await (0, run_1.runOsaScript)(`tell application "Microsoft Outlook"
|
|
264
269
|
set out to ""
|
|
265
270
|
repeat with theId in ${(0, run_1.asIdList)(candidates.map(c => c.id))}
|
|
@@ -268,26 +273,27 @@ return my scanFolder(rootInbox, rootName, cutoff, skipIds, skipNames, onlyNames,
|
|
|
268
273
|
set theMsg to message id theId
|
|
269
274
|
end try
|
|
270
275
|
if theMsg is not missing value then
|
|
271
|
-
${(0, scripts_1.senderSnippet)('
|
|
276
|
+
${(0, scripts_1.senderSnippet)(' ')}
|
|
272
277
|
set attNames to {}
|
|
273
278
|
try
|
|
274
279
|
set attNames to name of every attachment of theMsg
|
|
275
280
|
end try
|
|
276
281
|
${body}
|
|
277
|
-
set out to out & ${(0, run_1.asRow)(['(id of theMsg as string)', 'sndName', 'sndAddr', 'my
|
|
282
|
+
set out to out & ${(0, run_1.asRow)(['(id of theMsg as string)', 'sndName', 'sndAddr', 'my sanitizePositionalList(attNames)', 'bodyText'])}
|
|
278
283
|
end if
|
|
279
284
|
end repeat
|
|
280
285
|
return out
|
|
281
286
|
end tell`, 'scan');
|
|
282
|
-
|
|
287
|
+
const details = new Map((0, run_1.splitRecords)(raw).map(record => {
|
|
283
288
|
const parts = (0, run_1.splitFields)(record);
|
|
284
|
-
|
|
289
|
+
const detail = {
|
|
285
290
|
senderName: (0, run_1.field)(parts, 1),
|
|
286
291
|
senderEmail: (0, run_1.field)(parts, 2),
|
|
287
|
-
attachmentNames: (0, run_1.
|
|
292
|
+
attachmentNames: (0, run_1.splitPositionalList)((0, run_1.field)(parts, 3)),
|
|
288
293
|
body: (0, run_1.field)(parts, 4),
|
|
289
|
-
}
|
|
290
|
-
|
|
294
|
+
};
|
|
295
|
+
return [(0, run_1.field)(parts, 0), detail];
|
|
296
|
+
}));
|
|
291
297
|
const matches = [];
|
|
292
298
|
for (const candidate of candidates) {
|
|
293
299
|
const detail = details.get(candidate.id);
|
|
@@ -313,6 +319,10 @@ end tell`, 'scan');
|
|
|
313
319
|
/**
|
|
314
320
|
* The email currently selected — or open — in Outlook. `current messages`
|
|
315
321
|
* covers both cases the Windows reader handles separately.
|
|
322
|
+
*
|
|
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.
|
|
316
326
|
*/
|
|
317
327
|
async function readSelectedEmail() {
|
|
318
328
|
const records = (0, run_1.splitRecords)(await (0, run_1.runOsaScript)(`tell application "Microsoft Outlook"
|
|
@@ -323,11 +333,31 @@ async function readSelectedEmail() {
|
|
|
323
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.`)}
|
|
324
334
|
set theMsg to item 1 of sel
|
|
325
335
|
${(0, scripts_1.messageDetailSnippet)(true)}
|
|
326
|
-
|
|
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'])}
|
|
327
354
|
end tell`, 'quick'));
|
|
328
355
|
if (records.length === 0)
|
|
329
356
|
throw new errors_1.NotFoundError('email', 'Outlook reported no selected email.');
|
|
330
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)) : '');
|
|
331
361
|
return {
|
|
332
362
|
entryId: (0, run_1.field)(parts, scripts_1.MessageDetail.id),
|
|
333
363
|
storeId: '',
|
|
@@ -336,9 +366,17 @@ end tell`, 'quick'));
|
|
|
336
366
|
senderEmail: (0, run_1.field)(parts, scripts_1.MessageDetail.senderEmail),
|
|
337
367
|
receivedTime: (0, run_1.field)(parts, scripts_1.MessageDetail.receivedTime),
|
|
338
368
|
body: (0, run_1.field)(parts, scripts_1.MessageDetail.body),
|
|
339
|
-
attachmentNames: (0, run_1.
|
|
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)) : '',
|
|
340
371
|
};
|
|
341
372
|
}
|
|
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
|
+
}
|
|
342
380
|
/** Open an email in Outlook and bring Outlook forward. */
|
|
343
381
|
async function openOutlookEmail(email) {
|
|
344
382
|
const id = (0, scripts_1.macMessageId)(email.entryId);
|
package/dist/mac/read.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/mac/read.ts"],"names":[],"mappings":"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","sourcesContent":["// Reading mail on macOS: one folder, one item, a whole Inbox tree, or whatever\n// is selected in the running Outlook.\n//\n// Every reader is two passes. Pass 1 indexes a folder with bulk property reads\n// (one Apple event each); pass 2 fetches the expensive fields — body,\n// attachments, sender — only for the messages that survived. Reading\n// per-message in a loop costs an event per property and is unusably slow on a\n// real mailbox, which is the whole reason for the shape.\nimport {\n AS_LIST_SEP,\n asIdList,\n asInt,\n asRow,\n asString,\n field,\n runOsaScript,\n splitFields,\n splitList,\n splitRecords,\n summaryFields\n} from './run';\nimport {\n accountLookupSnippet,\n dateProperty,\n firstRecipientSnippet,\n macFolderPath,\n macMessageId,\n mailScopeSnippet,\n MessageDetail,\n messageDetailFields,\n messageDetailSnippet,\n messageLookupSnippet,\n resolveMacAccount,\n rootFolderSnippet,\n senderSnippet,\n} from './scripts';\nimport { failureTag, NotFoundError } from '../errors';\nimport { folderLeafName, isOutgoingRoot, REPLY_PREFIX, subjectGlobSource } from '../mail';\nimport type { RawEmail, ReadInboxRequest, SearchRequest } from '../backend';\nimport type { EmailLocator, InboxEmail, InboxSearchMatch, SelectedEmail } from '../types';\n\n/**\n * Recent mail from the Inbox root or one folder, newest first. Entry ids here\n * are Outlook for Mac's integer message ids, not Windows EntryIDs.\n */\nexport async function readInboxEmails(request: ReadInboxRequest): Promise<InboxEmail[]> {\n const acct = await resolveMacAccount(request.account);\n const folderPath = macFolderPath(request.account, request.folder.rootLabel, request.folder.segments);\n const outgoing = isOutgoingRoot(request.folder.rootId);\n\n // Pass 1 — index the folder. Its order isn't documented, so it is sorted here.\n const index = (await runOsaScript(`tell application \"Microsoft Outlook\"\n${accountLookupSnippet(acct)}\n${mailScopeSnippet(acct, request.folder, request.folderLabel)}\n set cutoff to (current date) - (${asInt(request.daysBack)} * days)\n set idList to id of every message of scopeFolder\n try\n set timeList to ${dateProperty(request.folder.rootId)} of every message of scopeFolder\n on error\n set timeList to {}\n end try\n set hasTimes to ((count of timeList) is (count of idList))\n set out to \"\"\n repeat with i from 1 to (count of idList)\n set d to missing value\n if hasTimes then set d to item i of timeList\n -- An item with no timestamp of its own (an unsent draft filed into a\n -- mail folder) is skipped, as the Windows date filter skips it.\n if d is not missing value and d is greater than or equal to cutoff then\n set out to out & (item i of idList as string) & tab & my isoDate(d) & linefeed\n end if\n end repeat\n return out\nend tell`, 'standard'))\n .split('\\n')\n .map(line => line.trim())\n .filter(Boolean)\n .map(line => {\n const [id, receivedTime] = line.split('\\t');\n return {id, receivedTime: receivedTime || ''};\n });\n // 'yyyy-MM-dd HH:mm' sorts correctly as a string.\n index.sort((a, b) => b.receivedTime.localeCompare(a.receivedTime));\n const chosen = index.slice(0, request.limit);\n if (chosen.length === 0) return [];\n\n // Pass 2 — the expensive reads, only for the messages kept.\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 set out to \"\"\n repeat with theId in ${asIdList(chosen.map(c => c.id))}\n set theMsg to missing value\n try\n set theMsg to message id theId\n end try\n if theMsg is not missing value then\n set subj to \"\"\n try\n set subj to (subject of theMsg) as string\n end try\n${outgoing ? firstRecipientSnippet('theMsg', ' ') : senderSnippet('theMsg', ' ')}\n${preview}\n set attNames to {}\n try\n set attNames to name of every attachment of theMsg\n end try\n set out to out & ${asRow(['(id of theMsg as string)', 'subj', 'sndName', 'sndAddr', 'bodyText', 'my sanitizeList(attNames)'])}\n end if\n end repeat\n return out\nend tell`, 'standard');\n\n const receivedById = new Map(chosen.map(c => [c.id, c.receivedTime]));\n return splitRecords(raw).map(record => {\n const parts = splitFields(record);\n const id = field(parts, 0);\n const attachmentNames = splitList(field(parts, 5));\n return {\n entryId: id,\n storeId: '',\n subject: field(parts, 1).trim(),\n senderName: field(parts, 2),\n senderEmail: field(parts, 3),\n receivedTime: receivedById.get(id) || '',\n bodyPreview: field(parts, 4),\n attachmentNames,\n attachmentCount: attachmentNames.length,\n folderPath,\n };\n });\n}\n\n/**\n * One email's full plain-text body. `storeId` has no macOS meaning: `message id\n * N` resolves against the application, wherever the message sits.\n */\nexport async function readEmailBody(email: EmailLocator): Promise<RawEmail> {\n const id = macMessageId(email.entryId);\n const parts = summaryFields(await runOsaScript(`tell application \"Microsoft Outlook\"\n${messageLookupSnippet(id)}\n${messageDetailSnippet(true)}\n return ${asRow(messageDetailFields(true))}\nend tell`, 'quick'));\n return {\n entryId: field(parts, MessageDetail.id) || id,\n subject: field(parts, MessageDetail.subject).trim(),\n senderName: field(parts, MessageDetail.senderName),\n senderEmail: field(parts, MessageDetail.senderEmail),\n receivedTime: field(parts, MessageDetail.receivedTime),\n body: field(parts, MessageDetail.body),\n attachmentNames: splitList(field(parts, MessageDetail.attachmentNames)),\n };\n}\n\n/**\n * Walk every folder under the Inbox for the mail matching the request.\n *\n * The filtering happens here rather than in Outlook: there is no AppleScript\n * counterpart to COM's server-side `Items.Restrict`, so each folder is indexed in\n * bulk reads and the subject and date tests run on the index. The results match\n * Windows; the cost doesn't, and a wide window over a big tree is heavier here.\n */\nexport async function searchInboxByFilter(request: SearchRequest): Promise<InboxSearchMatch[]> {\n const acct = await resolveMacAccount(request.account);\n // AppleScript reaches a child folder by name, so a path entry narrows to its leaf.\n const asNameList = (list: readonly string[]) => `{${list.map(folderLeafName).filter(Boolean).map(asString).join(', ')}}`;\n const rootInScope = request.includeFolders.length === 0 ? 'true' : 'false';\n // Sent, Drafts, Deleted and Junk hang UNDER the Inbox on an IMAP profile, so\n // their ids are collected first and the walk never descends into them — by\n // the profile's own id where it records one, through the account otherwise.\n const skipIds = ['sent items', 'deleted items', 'drafts', 'junk mail']\n .map(term => {\n const id = acct.folderIds?.[term];\n return id !== undefined\n ? ` set end of skipIds to ${id}`\n : ` try\n set end of skipIds to (id of (${term} of targetAcct))\n end try`;\n })\n .join('\\n');\n\n // Pass 1 — index every folder under the Inbox: path, id, subject, received.\n const indexed = splitRecords(await runOsaScript(`on scanFolder(theFolder, prefix, cutoff, skipIds, skipNames, onlyNames, inScope)\n set out to \"\"\n set idList to {}\n set subjList to {}\n set timeList to {}\n -- A folder the walk only passes through is never read: the bulk reads are\n -- the expensive part.\n if inScope then\n tell application \"Microsoft Outlook\"\n set idList to id of every message of theFolder\n set subjList to subject of every message of theFolder\n try\n set timeList to time received of every message of theFolder\n on error\n set timeList to {}\n end try\n end tell\n end if\n tell application \"Microsoft Outlook\"\n set subs to mail folders of theFolder\n end tell\n set hasTimes to ((count of timeList) is (count of idList))\n repeat with i from 1 to (count of idList)\n set d to missing value\n if hasTimes then set d to item i of timeList\n -- An item with no readable receive time is kept whatever the window\n -- says; dropping it would make a folder of them look empty.\n if d is missing value or d is not less than cutoff then\n set out to out & ${asRow(['prefix', '(item i of idList as string)', '(item i of subjList)', 'my isoDate(d)'])}\n end if\n end repeat\n repeat with f in subs\n set skipThis to false\n try\n if skipIds contains (id of f) then set skipThis to true\n end try\n try\n if skipNames contains ((name of f) as string) then set skipThis to true\n end try\n if not skipThis then\n set childName to \"\"\n try\n set childName to (name of f) as string\n end try\n set childScope to (count of onlyNames) is 0\n try\n if onlyNames contains childName then set childScope to true\n end try\n set out to out & my scanFolder(f, prefix & ${AS_LIST_SEP} & childName, cutoff, skipIds, skipNames, onlyNames, childScope)\n end if\n end repeat\n return out\nend scanFolder\n\ntell application \"Microsoft Outlook\"\n${accountLookupSnippet(acct)}\n${rootFolderSnippet(acct, 'inbox', 'rootInbox')}\n set rootName to (name of rootInbox) as string\n set skipIds to {}\n${skipIds}\n set skipNames to ${asNameList(request.excludeFolders)}\n set onlyNames to ${asNameList(request.includeFolders)}\nend tell\nset cutoff to (current date) - (${asInt(request.daysBack)} * days)\nreturn my scanFolder(rootInbox, rootName, cutoff, skipIds, skipNames, onlyNames, ${rootInScope})`, 'scan'));\n\n const subjectFiltered = !!(request.subjectLike || request.subjectPattern || request.excludeReplies);\n const likeRe = request.subjectLike ? new RegExp(subjectGlobSource(request.subjectLike), 'i') : null;\n // PowerShell's -match is case-insensitive, so the Windows test is too.\n const patternRe = request.subjectPattern ? new RegExp(request.subjectPattern.source, 'i') : null;\n const candidates = indexed\n .map(record => {\n const parts = splitFields(record);\n const segments = splitList(field(parts, 0));\n return {\n folderPath: macFolderPath(request.account, segments[0] || 'Inbox', segments.slice(1)),\n id: field(parts, 1),\n subject: field(parts, 2).trim(),\n receivedTime: field(parts, 3),\n };\n })\n .filter(candidate => {\n if (subjectFiltered && !candidate.subject) return false;\n if (likeRe && !likeRe.test(candidate.subject)) return false;\n if (patternRe && !patternRe.test(candidate.subject)) return false;\n if (request.excludeReplies && REPLY_PREFIX.test(candidate.subject)) return false;\n return true;\n });\n if (candidates.length === 0) return [];\n\n // Pass 2 — senders, attachment names and (when asked) bodies, for the survivors only.\n const body = request.includeBody\n ? ` set bodyText to \"\"\n try\n set bodyText to (plain text content of theMsg) as string\n end try`\n : ' set bodyText to \"\"';\n const details = new Map<string, {\n senderName: string;\n senderEmail: string;\n attachmentNames: string[];\n body: string\n }>();\n const raw = await runOsaScript(`tell application \"Microsoft Outlook\"\n set out to \"\"\n repeat with theId in ${asIdList(candidates.map(c => c.id))}\n set theMsg to missing value\n try\n set theMsg to message id theId\n end try\n if theMsg is not missing value then\n${senderSnippet('theMsg', ' ')}\n set attNames to {}\n try\n set attNames to name of every attachment of theMsg\n end try\n${body}\n set out to out & ${asRow(['(id of theMsg as string)', 'sndName', 'sndAddr', 'my sanitizeList(attNames)', 'bodyText'])}\n end if\n end repeat\n return out\nend tell`, 'scan');\n for (const record of splitRecords(raw)) {\n const parts = splitFields(record);\n details.set(field(parts, 0), {\n senderName: field(parts, 1),\n senderEmail: field(parts, 2),\n attachmentNames: splitList(field(parts, 3)),\n body: field(parts, 4),\n });\n }\n\n const matches: InboxSearchMatch[] = [];\n for (const candidate of candidates) {\n const detail = details.get(candidate.id);\n if (!detail) continue;\n // Attachment names only arrive with the details, so this test waits for them.\n if (request.requireAttachment && detail.attachmentNames.length === 0) continue;\n matches.push({\n entryId: candidate.id,\n storeId: '',\n subject: candidate.subject,\n senderName: detail.senderName,\n senderEmail: detail.senderEmail,\n receivedTime: candidate.receivedTime,\n body: detail.body,\n attachmentNames: detail.attachmentNames,\n folderPath: candidate.folderPath,\n });\n }\n return matches;\n}\n\n/**\n * The email currently selected — or open — in Outlook. `current messages`\n * covers both cases the Windows reader handles separately.\n */\nexport async function readSelectedEmail(): Promise<SelectedEmail> {\n const records = splitRecords(await runOsaScript(`tell application \"Microsoft Outlook\"\n set sel to {}\n try\n set sel to current messages\n end try\n if (count of sel) is 0 then error ${asString(`${failureTag('NOT_FOUND', 'email')}No email is selected in Outlook. Select or open an email, then try again.`)}\n set theMsg to item 1 of sel\n${messageDetailSnippet(true)}\n return ${asRow(messageDetailFields(true))}\nend tell`, 'quick'));\n if (records.length === 0) throw new NotFoundError('email', 'Outlook reported no selected email.');\n const parts = splitFields(records[0]);\n return {\n entryId: field(parts, MessageDetail.id),\n storeId: '',\n subject: field(parts, MessageDetail.subject).trim(),\n senderName: field(parts, MessageDetail.senderName),\n senderEmail: field(parts, MessageDetail.senderEmail),\n receivedTime: field(parts, MessageDetail.receivedTime),\n body: field(parts, MessageDetail.body),\n attachmentNames: splitList(field(parts, MessageDetail.attachmentNames)),\n };\n}\n\n/** Open an email in Outlook and bring Outlook forward. */\nexport async function openOutlookEmail(email: EmailLocator): Promise<void> {\n const id = macMessageId(email.entryId);\n await runOsaScript(`\ntell application \"Microsoft Outlook\"\n${messageLookupSnippet(id)}\n open theMsg\n activate\nend tell`, 'quick');\n}\n"]}
|
|
1
|
+
{"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/mac/read.ts"],"names":[],"mappings":"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","sourcesContent":["// Reading mail on macOS: one folder, one item, a whole Inbox tree, or whatever\n// is selected in the running Outlook.\n//\n// Every reader is two passes. Pass 1 indexes a folder with bulk property reads\n// (one Apple event each); pass 2 fetches the expensive fields — body,\n// attachments, sender — only for the messages that survived. Reading\n// per-message in a loop costs an event per property and is unusably slow on a\n// real mailbox, which is the whole reason for the shape.\nimport {\n AS_LIST_SEP,\n asIdList,\n asInt,\n asRow,\n asString,\n field,\n runOsaScript,\n splitFields,\n splitList,\n splitPositionalList,\n splitRecords,\n summaryFields,\n} from './run';\nimport { readProfileAccounts } from './profile';\nimport {\n accountLookupSnippet,\n dateProperty,\n firstRecipientSnippet,\n folderChainSnippet,\n macFolderPath,\n macMessageId,\n mailScopeSnippet,\n MessageDetail,\n messageDetailFields,\n messageDetailSnippet,\n messageLookupSnippet,\n resolveMacAccount,\n rootFolderSnippet,\n senderSnippet,\n} from './scripts';\nimport { failureTag, NotFoundError } from '../errors';\nimport { folderLeafName, isOutgoingRoot, REPLY_PREFIX, subjectGlobSource } from '../mail';\nimport type { RawEmail, ReadInboxRequest, SearchRequest } from '../backend';\nimport type { EmailLocator, InboxEmail, InboxSearchMatch, SelectedEmail } from '../types';\n\n/**\n * Recent mail from the Inbox root or one folder, newest first. Entry ids here\n * are Outlook for Mac's integer message ids, not Windows EntryIDs.\n */\nexport async function readInboxEmails(request: ReadInboxRequest): Promise<InboxEmail[]> {\n const acct = await resolveMacAccount(request.account);\n const folderPath = macFolderPath(request.account, request.folder.rootLabel, request.folder.segments);\n const outgoing = isOutgoingRoot(request.folder.rootId);\n\n // Pass 1 — index the folder. Its order isn't documented, so it is sorted here.\n const index = (await runOsaScript(`tell application \"Microsoft Outlook\"\n${accountLookupSnippet(acct)}\n${mailScopeSnippet(acct, request.folder, request.folderLabel)}\n set cutoff to (current date) - (${asInt(request.daysBack)} * days)\n set idList to id of every message of scopeFolder\n try\n set timeList to ${dateProperty(request.folder.rootId)} of every message of scopeFolder\n on error\n set timeList to {}\n end try\n set hasTimes to ((count of timeList) is (count of idList))\n set out to \"\"\n repeat with i from 1 to (count of idList)\n set d to missing value\n if hasTimes then set d to item i of timeList\n -- An item with no timestamp of its own (an unsent draft filed into a\n -- mail folder) is skipped, as the Windows date filter skips it.\n if d is not missing value and d is greater than or equal to cutoff then\n set out to out & (item i of idList as string) & tab & my isoDate(d) & linefeed\n end if\n end repeat\n return out\nend tell`, 'standard'))\n .split('\\n')\n .map(line => line.trim())\n .filter(Boolean)\n .map(line => {\n const [id, receivedTime] = line.split('\\t');\n return { id, receivedTime: receivedTime || '' };\n });\n // 'yyyy-MM-dd HH:mm' sorts correctly as a string.\n index.sort((a, b) => b.receivedTime.localeCompare(a.receivedTime));\n const chosen = index.slice(0, request.limit);\n if (chosen.length === 0) return [];\n\n // Pass 2 — the expensive reads, only for the messages kept.\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 set out to \"\"\n repeat with theId in ${asIdList(chosen.map(c => c.id))}\n set theMsg to missing value\n try\n set theMsg to message id theId\n end try\n if theMsg is not missing value then\n set subj to \"\"\n try\n set subj to (subject of theMsg) as string\n end try\n${outgoing ? firstRecipientSnippet(' ') : senderSnippet(' ')}\n${preview}\n set attNames to {}\n try\n set attNames to name of every attachment of theMsg\n end try\n set out to out & ${asRow(['(id of theMsg as string)', 'subj', 'sndName', 'sndAddr', 'bodyText', 'my sanitizePositionalList(attNames)'])}\n end if\n end repeat\n return out\nend tell`, 'standard');\n\n const receivedById = new Map(chosen.map(c => [c.id, c.receivedTime]));\n return splitRecords(raw).map(record => {\n const parts = splitFields(record);\n const id = field(parts, 0);\n const attachmentNames = splitPositionalList(field(parts, 5));\n return {\n entryId: id,\n storeId: '',\n subject: field(parts, 1).trim(),\n senderName: field(parts, 2),\n senderEmail: field(parts, 3),\n receivedTime: receivedById.get(id) || '',\n bodyPreview: field(parts, 4),\n attachmentNames,\n attachmentCount: attachmentNames.length,\n folderPath,\n };\n });\n}\n\n/**\n * One email's full plain-text body. `storeId` has no macOS meaning: `message id\n * N` resolves against the application, wherever the message sits.\n */\nexport async function readEmailBody(email: EmailLocator): Promise<RawEmail> {\n const id = macMessageId(email.entryId);\n const parts = summaryFields(await runOsaScript(`tell application \"Microsoft Outlook\"\n${messageLookupSnippet(id)}\n${messageDetailSnippet(true)}\n return ${asRow(messageDetailFields(true))}\nend tell`, 'quick'));\n return {\n entryId: field(parts, MessageDetail.id) || id,\n subject: field(parts, MessageDetail.subject).trim(),\n senderName: field(parts, MessageDetail.senderName),\n senderEmail: field(parts, MessageDetail.senderEmail),\n receivedTime: field(parts, MessageDetail.receivedTime),\n body: field(parts, MessageDetail.body),\n attachmentNames: splitPositionalList(field(parts, MessageDetail.attachmentNames)),\n };\n}\n\n/**\n * Walk every folder under the Inbox for the mail matching the request.\n *\n * The filtering happens here rather than in Outlook: there is no AppleScript\n * counterpart to COM's server-side `Items.Restrict`, so each folder is indexed in\n * bulk reads and the subject and date tests run on the index. The results match\n * Windows; the cost doesn't, and a wide window over a big tree is heavier here.\n */\nexport async function searchInboxByFilter(request: SearchRequest): Promise<InboxSearchMatch[]> {\n const acct = await resolveMacAccount(request.account);\n // AppleScript reaches a child folder by name, so a path entry narrows to its leaf.\n const asNameList = (list: readonly string[]) => `{${list.map(folderLeafName).filter(Boolean).map(asString).join(', ')}}`;\n const rootInScope = request.includeFolders.length === 0 ? 'true' : 'false';\n // Sent, Drafts, Deleted and Junk hang UNDER the Inbox on an IMAP profile, so\n // their ids are collected first and the walk never descends into them — by\n // the profile's own id where it records one, through the account otherwise.\n const skipIds = ['sent items', 'deleted items', 'drafts', 'junk mail']\n .map(term => {\n const id = acct.folderIds?.[term];\n return id !== undefined\n ? ` set end of skipIds to ${id}`\n : ` try\n set end of skipIds to (id of (${term} of targetAcct))\n end try`;\n })\n .join('\\n');\n\n // Pass 1 — index every folder under the Inbox: path, id, subject, received.\n const indexed = splitRecords(await runOsaScript(`on scanFolder(theFolder, prefix, cutoff, skipIds, skipNames, onlyNames, inScope)\n set out to \"\"\n set idList to {}\n set subjList to {}\n set timeList to {}\n -- A folder the walk only passes through is never read: the bulk reads are\n -- the expensive part.\n if inScope then\n tell application \"Microsoft Outlook\"\n set idList to id of every message of theFolder\n set subjList to subject of every message of theFolder\n try\n set timeList to time received of every message of theFolder\n on error\n set timeList to {}\n end try\n end tell\n end if\n tell application \"Microsoft Outlook\"\n set subs to mail folders of theFolder\n end tell\n set hasTimes to ((count of timeList) is (count of idList))\n repeat with i from 1 to (count of idList)\n set d to missing value\n if hasTimes then set d to item i of timeList\n -- An item with no readable receive time is kept whatever the window\n -- says; dropping it would make a folder of them look empty.\n if d is missing value or d is not less than cutoff then\n set out to out & ${asRow(['prefix', '(item i of idList as string)', '(item i of subjList)', 'my isoDate(d)'])}\n end if\n end repeat\n repeat with f in subs\n set skipThis to false\n try\n if skipIds contains (id of f) then set skipThis to true\n end try\n try\n if skipNames contains ((name of f) as string) then set skipThis to true\n end try\n if not skipThis then\n set childName to \"\"\n try\n set childName to (name of f) as string\n end try\n set childScope to (count of onlyNames) is 0\n try\n if onlyNames contains childName then set childScope to true\n end try\n set out to out & my scanFolder(f, prefix & ${AS_LIST_SEP} & childName, cutoff, skipIds, skipNames, onlyNames, childScope)\n end if\n end repeat\n return out\nend scanFolder\n\ntell application \"Microsoft Outlook\"\n${accountLookupSnippet(acct)}\n${rootFolderSnippet(acct, 'inbox', 'rootInbox')}\n set rootName to (name of rootInbox) as string\n set skipIds to {}\n${skipIds}\n set skipNames to ${asNameList(request.excludeFolders)}\n set onlyNames to ${asNameList(request.includeFolders)}\nend tell\nset cutoff to (current date) - (${asInt(request.daysBack)} * days)\nreturn my scanFolder(rootInbox, rootName, cutoff, skipIds, skipNames, onlyNames, ${rootInScope})`, 'scan'));\n\n const subjectFiltered = !!(request.subjectLike || request.subjectPattern || request.excludeReplies);\n const likeRe = request.subjectLike ? new RegExp(subjectGlobSource(request.subjectLike), 'i') : null;\n // PowerShell's -match is case-insensitive, so this test is too, whatever the\n // caller's flags. Only u and v are kept: they decide how the source parses,\n // and without u, /\\p{L}/u would match the literal text \"p{L}\". Keeping g or y\n // would carry lastIndex from one subject to the next.\n const patternRe = request.subjectPattern\n ? new RegExp(request.subjectPattern.source, 'i' + request.subjectPattern.flags.replace(/[^uv]/g, ''))\n : null;\n const candidates = indexed\n .map(record => {\n const parts = splitFields(record);\n const segments = splitList(field(parts, 0));\n return {\n folderPath: macFolderPath(request.account, segments[0] || 'Inbox', segments.slice(1)),\n id: field(parts, 1),\n subject: field(parts, 2).trim(),\n receivedTime: field(parts, 3),\n };\n })\n .filter(candidate => {\n if (subjectFiltered && !candidate.subject) return false;\n if (likeRe && !likeRe.test(candidate.subject)) return false;\n if (patternRe && !patternRe.test(candidate.subject)) return false;\n if (request.excludeReplies && REPLY_PREFIX.test(candidate.subject)) return false;\n return true;\n });\n if (candidates.length === 0) return [];\n\n // Pass 2 — senders, attachment names and (when asked) bodies, for the survivors only.\n const body = request.includeBody\n ? ` set bodyText to \"\"\n try\n set bodyText to (plain text content of theMsg) as string\n end try`\n : ' set bodyText to \"\"';\n const raw = await runOsaScript(`tell application \"Microsoft Outlook\"\n set out to \"\"\n repeat with theId in ${asIdList(candidates.map(c => c.id))}\n set theMsg to missing value\n try\n set theMsg to message id theId\n end try\n if theMsg is not missing value then\n${senderSnippet(' ')}\n set attNames to {}\n try\n set attNames to name of every attachment of theMsg\n end try\n${body}\n set out to out & ${asRow(['(id of theMsg as string)', 'sndName', 'sndAddr', 'my sanitizePositionalList(attNames)', 'bodyText'])}\n end if\n end repeat\n return out\nend tell`, 'scan');\n const details = new Map(splitRecords(raw).map(record => {\n const parts = splitFields(record);\n const detail = {\n senderName: field(parts, 1),\n senderEmail: field(parts, 2),\n attachmentNames: splitPositionalList(field(parts, 3)),\n body: field(parts, 4),\n };\n return [field(parts, 0), detail] as const;\n }));\n\n const matches: InboxSearchMatch[] = [];\n for (const candidate of candidates) {\n const detail = details.get(candidate.id);\n if (!detail) continue;\n // Attachment names only arrive with the details, so this test waits for them.\n if (request.requireAttachment && detail.attachmentNames.length === 0) continue;\n matches.push({\n entryId: candidate.id,\n storeId: '',\n subject: candidate.subject,\n senderName: detail.senderName,\n senderEmail: detail.senderEmail,\n receivedTime: candidate.receivedTime,\n body: detail.body,\n attachmentNames: detail.attachmentNames,\n folderPath: candidate.folderPath,\n });\n }\n return matches;\n}\n\n/**\n * The email currently selected — or open — in Outlook. `current messages`\n * covers both cases the Windows reader handles separately.\n *\n * Where it sits comes back as a second record, so the detail row keeps its body\n * last. A mailbox with no account object answers `account of` with missing\n * value; its root folder's id is then looked up in the profile database.\n */\nexport async function readSelectedEmail(): Promise<SelectedEmail> {\n const records = splitRecords(await runOsaScript(`tell application \"Microsoft Outlook\"\n set sel to {}\n try\n set sel to current messages\n end try\n if (count of sel) is 0 then error ${asString(`${failureTag('NOT_FOUND', 'email')}No email is selected in Outlook. Select or open an email, then try again.`)}\n set theMsg to item 1 of sel\n${messageDetailSnippet(true)}\n set acctAddr to \"\"\n try\n set rawAddr to email address of (account of theMsg)\n if rawAddr is not missing value then set acctAddr to rawAddr as string\n end try\n set chainNames to {}\n set rootId to \"\"\n set theFolder to missing value\n try\n set theFolder to folder of theMsg\n end try\n if theFolder is not missing value then\n${folderChainSnippet(' ')}\n try\n set rootId to (id of cur) as string\n end try\n end if\n return ${asRow(messageDetailFields(true))} & ${asRow(['acctAddr', 'my sanitizeList(chainNames)', 'rootId'])}\nend tell`, 'quick'));\n if (records.length === 0) throw new NotFoundError('email', 'Outlook reported no selected email.');\n const parts = splitFields(records[0]);\n const where = splitFields(records[1] ?? '');\n const chain = splitList(field(where, 1)).reverse();\n const account = field(where, 0) || (chain.length ? await accountByRootFolder(field(where, 2)) : '');\n return {\n entryId: field(parts, MessageDetail.id),\n storeId: '',\n subject: field(parts, MessageDetail.subject).trim(),\n senderName: field(parts, MessageDetail.senderName),\n senderEmail: field(parts, MessageDetail.senderEmail),\n receivedTime: field(parts, MessageDetail.receivedTime),\n body: field(parts, MessageDetail.body),\n attachmentNames: splitPositionalList(field(parts, MessageDetail.attachmentNames)),\n folderPath: account && chain.length ? macFolderPath(account, chain[0], chain.slice(1)) : '',\n };\n}\n\n/** The address of the account whose root folder has this id, per the profile database; '' if none. */\nasync function accountByRootFolder(rootId: string): Promise<string> {\n if (!/^\\d+$/.test(rootId)) return '';\n const id = Number(rootId);\n return (await readProfileAccounts()).find(a => a.folderIds['root folder'] === id)?.emailAccount ?? '';\n}\n\n/** Open an email in Outlook and bring Outlook forward. */\nexport async function openOutlookEmail(email: EmailLocator): Promise<void> {\n const id = macMessageId(email.entryId);\n await runOsaScript(`\ntell application \"Microsoft Outlook\"\n${messageLookupSnippet(id)}\n open theMsg\n activate\nend tell`, 'quick');\n}\n"]}
|
package/dist/mac/run.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type RunBudget } from '../runtime';
|
|
2
|
+
import type { ItemFailure } from '../types';
|
|
2
3
|
/**
|
|
3
4
|
* Run a script from a temp file (no argument-length or quoting limits) and
|
|
4
5
|
* return what it printed, minus the newline osascript appends.
|
|
@@ -43,13 +44,22 @@ export declare function asRow(fields: readonly string[]): string;
|
|
|
43
44
|
* `sanitize` strips the framing characters; `joinList` and `replaceText` are the
|
|
44
45
|
* text-item-delimiter dance nobody should write twice.
|
|
45
46
|
*/
|
|
46
|
-
export declare const AS_HANDLERS = "\non pad2(n)\n set s to (n as integer) as string\n if (length of s) < 2 then set s to \"0\" & s\n return s\nend pad2\n\non isoDate(d)\n if d is missing value then return \"\"\n return (year of d as string) & \"-\" & pad2(month of d as integer) & \"-\" & pad2(day of d) & \" \" & pad2(hours of d) & \":\" & pad2(minutes of d)\nend isoDate\n\non joinList(lst, sep)\n set saved to AppleScript's text item delimiters\n set AppleScript's text item delimiters to sep\n set out to lst as string\n set AppleScript's text item delimiters to saved\n return out\nend joinList\n\non replaceText(t, findWhat, replaceWith)\n set saved to AppleScript's text item delimiters\n set AppleScript's text item delimiters to findWhat\n set parts to text items of t\n set AppleScript's text item delimiters to replaceWith\n set out to parts as string\n set AppleScript's text item delimiters to saved\n return out\nend replaceText\n\n-- Strips the FIELD and RECORD separators only. The LIST separator is left\n-- alone on purpose: a field may legitimately BE a list.\non sanitize(v)\n if v is missing value then return \"\"\n set t to v as string\n repeat with codePoint in {30, 31}\n set marker to (character id (codePoint as integer))\n if t contains marker then set t to my replaceText(t, marker, \" \")\n end repeat\n return t\nend sanitize\n\n-- A list field: each element loses all three separators, then they are joined\n-- with the LIST separator that splitList() takes them apart on.\non sanitizeList(lst)\n set cleaned to {}\n repeat with item_ in lst\n set t to my sanitize(item_)\n set marker to (character id 29)\n if t contains marker then set t to my replaceText(t, marker, \" \")\n set end of cleaned to t\n end repeat\n return my joinList(cleaned, (character id 29))\nend sanitizeList\n\non insertAboveQuoted(c, ins)\n if c is missing value then return ins\n set c to c as string\n if (length of c) is 0 then return ins\n -- A reply's own empty paragraph sits inside WordSection1; inserting straight\n -- after that tag puts the new text where the cursor would be. Anything else\n -- falls back to just inside <body>. ('at' is a reserved parameter name, so\n -- the cut point can't be called that.)\n set cutPoint to my tagEnd(c, \"WordSection1>\")\n if cutPoint is 0 then set cutPoint to my tagEnd(c, \"<body\")\n if cutPoint is 0 then return ins & c\n if cutPoint is (length of c) then return c & ins\n return (text 1 thru cutPoint of c) & ins & (text (cutPoint + 1) thru -1 of c)\nend insertAboveQuoted\n\n-- 'rest' is an AppleScript term, so the remainder can't be called that:\n-- assigning to it compiles and then fails at run time.\non tagEnd(c, marker)\n set ix to offset of marker in c\n if ix is 0 then return 0\n if marker ends with \">\" then return ix + (length of marker) - 1\n set tailText to text ix thru -1 of c\n set gt to offset of \">\" in tailText\n if gt is 0 then return 0\n return ix + gt - 1\nend tagEnd\n";
|
|
47
|
+
export declare const AS_HANDLERS = "\non pad2(n)\n set s to (n as integer) as string\n if (length of s) < 2 then set s to \"0\" & s\n return s\nend pad2\n\non isoDate(d)\n if d is missing value then return \"\"\n return (year of d as string) & \"-\" & pad2(month of d as integer) & \"-\" & pad2(day of d) & \" \" & pad2(hours of d) & \":\" & pad2(minutes of d)\nend isoDate\n\non joinList(lst, sep)\n set saved to AppleScript's text item delimiters\n set AppleScript's text item delimiters to sep\n set out to lst as string\n set AppleScript's text item delimiters to saved\n return out\nend joinList\n\non replaceText(t, findWhat, replaceWith)\n set saved to AppleScript's text item delimiters\n set AppleScript's text item delimiters to findWhat\n set parts to text items of t\n set AppleScript's text item delimiters to replaceWith\n set out to parts as string\n set AppleScript's text item delimiters to saved\n return out\nend replaceText\n\n-- Strips the FIELD and RECORD separators only. The LIST separator is left\n-- alone on purpose: a field may legitimately BE a list.\non sanitize(v)\n if v is missing value then return \"\"\n set t to v as string\n repeat with codePoint in {30, 31}\n set marker to (character id (codePoint as integer))\n if t contains marker then set t to my replaceText(t, marker, \" \")\n end repeat\n return t\nend sanitize\n\n-- A list field: each element loses all three separators, then they are joined\n-- with the LIST separator that splitList() takes them apart on.\non sanitizeList(lst)\n set cleaned to {}\n repeat with item_ in lst\n set t to my sanitize(item_)\n set marker to (character id 29)\n if t contains marker then set t to my replaceText(t, marker, \" \")\n set end of cleaned to t\n end repeat\n return my joinList(cleaned, (character id 29))\nend sanitizeList\n\n-- A list whose positions matter, like an email's attachments: every element is\n-- FOLLOWED by the LIST separator, so an empty element keeps its place and an\n-- empty list stays distinct from a list of one empty name. splitPositionalList()\n-- takes it apart.\non sanitizePositionalList(lst)\n if (count of lst) is 0 then return \"\"\n return my sanitizeList(lst) & (character id 29)\nend sanitizePositionalList\n\non insertAboveQuoted(c, ins)\n if c is missing value then return ins\n set c to c as string\n if (length of c) is 0 then return ins\n -- A reply's own empty paragraph sits inside WordSection1; inserting straight\n -- after that tag puts the new text where the cursor would be. Anything else\n -- falls back to just inside <body>. ('at' is a reserved parameter name, so\n -- the cut point can't be called that.)\n set cutPoint to my tagEnd(c, \"WordSection1>\")\n if cutPoint is 0 then set cutPoint to my tagEnd(c, \"<body\")\n if cutPoint is 0 then return ins & c\n if cutPoint is (length of c) then return c & ins\n return (text 1 thru cutPoint of c) & ins & (text (cutPoint + 1) thru -1 of c)\nend insertAboveQuoted\n\n-- 'rest' is an AppleScript term, so the remainder can't be called that:\n-- assigning to it compiles and then fails at run time.\non tagEnd(c, marker)\n set ix to offset of marker in c\n if ix is 0 then return 0\n if marker ends with \">\" then return ix + (length of marker) - 1\n set tailText to text ix thru -1 of c\n set gt to offset of \">\" in tailText\n if gt is 0 then return 0\n return ix + gt - 1\nend tagEnd\n";
|
|
47
48
|
/** A script's output as records, without the trailing empty one. */
|
|
48
49
|
export declare function splitRecords(raw: string): string[];
|
|
49
50
|
/** One record's fields. */
|
|
50
51
|
export declare function splitFields(record: string): string[];
|
|
51
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* A field that carries a list. Empty elements are dropped, so where an index
|
|
54
|
+
* into the list must match Outlook's, the field is written with
|
|
55
|
+
* `sanitizePositionalList` and read with `splitPositionalList` instead.
|
|
56
|
+
*/
|
|
52
57
|
export declare function splitList(value: string | undefined): string[];
|
|
58
|
+
/**
|
|
59
|
+
* A field written by `sanitizePositionalList`: every element at its position,
|
|
60
|
+
* empty ones included, so index N here is item N+1 of the AppleScript list.
|
|
61
|
+
*/
|
|
62
|
+
export declare function splitPositionalList(value: string | undefined): string[];
|
|
53
63
|
/** A field by position; a missing field reads as ''. */
|
|
54
64
|
export declare function field(fields: readonly string[], index: number): string;
|
|
55
65
|
/** The fields of a script's first record — the summary a mutation returns ahead of any per-item rows. */
|
|
@@ -58,3 +68,10 @@ export declare function summaryFields(raw: string): string[];
|
|
|
58
68
|
export declare function intField(fields: readonly string[], index: number, fallback?: number): number;
|
|
59
69
|
/** A boolean field; AppleScript renders booleans as the bare words true/false. */
|
|
60
70
|
export declare function boolField(fields: readonly string[], index: number): boolean;
|
|
71
|
+
/** The record a batch script appends, inside `on error errText`, for the id `theId` it failed on. */
|
|
72
|
+
export declare const ITEM_FAILURE_ROW: string;
|
|
73
|
+
/** A batch script's output: the summary record's fields, and the items that failed. */
|
|
74
|
+
export declare function splitBatch(raw: string): {
|
|
75
|
+
summary: string[];
|
|
76
|
+
failed: ItemFailure[];
|
|
77
|
+
};
|
package/dist/mac/run.js
CHANGED
|
@@ -3,7 +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.AS_HANDLERS = exports.AS_LIST_SEP = exports.AS_RECORD_SEP = exports.AS_FIELD_SEP = exports.LIST_SEP = exports.RECORD_SEP = exports.FIELD_SEP = void 0;
|
|
6
|
+
exports.ITEM_FAILURE_ROW = exports.AS_HANDLERS = exports.AS_LIST_SEP = exports.AS_RECORD_SEP = exports.AS_FIELD_SEP = exports.LIST_SEP = exports.RECORD_SEP = exports.FIELD_SEP = void 0;
|
|
7
7
|
exports.runOsaScript = runOsaScript;
|
|
8
8
|
exports.asEscape = asEscape;
|
|
9
9
|
exports.asString = asString;
|
|
@@ -14,10 +14,12 @@ exports.asRow = asRow;
|
|
|
14
14
|
exports.splitRecords = splitRecords;
|
|
15
15
|
exports.splitFields = splitFields;
|
|
16
16
|
exports.splitList = splitList;
|
|
17
|
+
exports.splitPositionalList = splitPositionalList;
|
|
17
18
|
exports.field = field;
|
|
18
19
|
exports.summaryFields = summaryFields;
|
|
19
20
|
exports.intField = intField;
|
|
20
21
|
exports.boolField = boolField;
|
|
22
|
+
exports.splitBatch = splitBatch;
|
|
21
23
|
// Getting a generated AppleScript to osascript and its output back.
|
|
22
24
|
//
|
|
23
25
|
// The macOS counterpart of windows/run.ts: running a script, the escaping
|
|
@@ -186,6 +188,15 @@ on sanitizeList(lst)
|
|
|
186
188
|
return my joinList(cleaned, (character id 29))
|
|
187
189
|
end sanitizeList
|
|
188
190
|
|
|
191
|
+
-- A list whose positions matter, like an email's attachments: every element is
|
|
192
|
+
-- FOLLOWED by the LIST separator, so an empty element keeps its place and an
|
|
193
|
+
-- empty list stays distinct from a list of one empty name. splitPositionalList()
|
|
194
|
+
-- takes it apart.
|
|
195
|
+
on sanitizePositionalList(lst)
|
|
196
|
+
if (count of lst) is 0 then return ""
|
|
197
|
+
return my sanitizeList(lst) & (character id 29)
|
|
198
|
+
end sanitizePositionalList
|
|
199
|
+
|
|
189
200
|
on insertAboveQuoted(c, ins)
|
|
190
201
|
if c is missing value then return ins
|
|
191
202
|
set c to c as string
|
|
@@ -221,10 +232,21 @@ function splitRecords(raw) {
|
|
|
221
232
|
function splitFields(record) {
|
|
222
233
|
return record.split(exports.FIELD_SEP);
|
|
223
234
|
}
|
|
224
|
-
/**
|
|
235
|
+
/**
|
|
236
|
+
* A field that carries a list. Empty elements are dropped, so where an index
|
|
237
|
+
* into the list must match Outlook's, the field is written with
|
|
238
|
+
* `sanitizePositionalList` and read with `splitPositionalList` instead.
|
|
239
|
+
*/
|
|
225
240
|
function splitList(value) {
|
|
226
241
|
return (value || '').split(exports.LIST_SEP).filter(Boolean);
|
|
227
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* A field written by `sanitizePositionalList`: every element at its position,
|
|
245
|
+
* empty ones included, so index N here is item N+1 of the AppleScript list.
|
|
246
|
+
*/
|
|
247
|
+
function splitPositionalList(value) {
|
|
248
|
+
return value ? value.split(exports.LIST_SEP).slice(0, -1) : [];
|
|
249
|
+
}
|
|
228
250
|
/** A field by position; a missing field reads as ''. */
|
|
229
251
|
function field(fields, index) {
|
|
230
252
|
return fields[index] ?? '';
|
|
@@ -242,4 +264,22 @@ function intField(fields, index, fallback = 0) {
|
|
|
242
264
|
function boolField(fields, index) {
|
|
243
265
|
return field(fields, index).trim() === 'true';
|
|
244
266
|
}
|
|
267
|
+
// ── Batches ──────────────────────────────────────────────────────────────
|
|
268
|
+
//
|
|
269
|
+
// A script acting on a list of ids returns one summary record, then one record
|
|
270
|
+
// per id that failed. The failure row and its decoder sit together so the two
|
|
271
|
+
// can't come to disagree about the field order.
|
|
272
|
+
/** The record a batch script appends, inside `on error errText`, for the id `theId` it failed on. */
|
|
273
|
+
exports.ITEM_FAILURE_ROW = asRow(['(theId as string)', 'subj', 'errText']);
|
|
274
|
+
/** A batch script's output: the summary record's fields, and the items that failed. */
|
|
275
|
+
function splitBatch(raw) {
|
|
276
|
+
const [summary = '', ...failures] = splitRecords(raw);
|
|
277
|
+
return {
|
|
278
|
+
summary: splitFields(summary),
|
|
279
|
+
failed: failures.map(record => {
|
|
280
|
+
const parts = splitFields(record);
|
|
281
|
+
return { entryId: field(parts, 0), subject: field(parts, 1), error: field(parts, 2) };
|
|
282
|
+
}),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
245
285
|
//# sourceMappingURL=run.js.map
|
package/dist/mac/run.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/mac/run.ts"],"names":[],"mappings":"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","sourcesContent":["// Getting a generated AppleScript to osascript and its output back.\n//\n// The macOS counterpart of windows/run.ts: running a script, the escaping\n// rules, and the record framing every reader here shares.\nimport fs from 'fs';\nimport { removeTempFile, type RunBudget, tempFile } from '../runtime';\nimport { runScriptProcess } from '../shared/exec';\nimport { InvalidRequestError } from '../errors';\nimport { WORD_SECTION_ANCHOR } from '../shared/replyBody';\n\nconst RUNNER = 'osascript' as const;\n\n/**\n * osascript's report of a script error: \"<file>:<start>:<end>: execution error:\n * <message> (<number>)\". Only the message is worth showing — these reach people\n * in per-item failure lists — and -2700 is the number AppleScript gives every\n * `error \"…\"` a script raises itself.\n */\nfunction parseFailure(stderr: string): { message: string } {\n const message = stderr\n .replace(/^(?:.*?:)?\\d+:\\d+:\\s*execution error:\\s*/m, '')\n .replace(/\\s*\\(-2700\\)\\s*$/, '')\n .trim();\n return {message};\n}\n\n/**\n * Run a script from a temp file (no argument-length or quoting limits) and\n * return what it printed, minus the newline osascript appends.\n *\n * `AS_HANDLERS` is prepended here rather than by each caller: a script missing\n * a handler fails to compile as a WHOLE, with an error pointing nowhere near the\n * omission, so it can't be left to each script to remember.\n */\nexport async function runOsaScript(source: string, budget: RunBudget): Promise<string> {\n // The full text is what osascript compiles, so it is also what error offsets\n // refer to and what the debug hook and errors carry.\n const script = AS_HANDLERS + source;\n const file = tempFile('osa', 'applescript');\n fs.writeFileSync(file, script, 'utf8');\n try {\n const {stdout} = await runScriptProcess({\n runner: RUNNER,\n command: 'osascript',\n args: [file],\n script,\n budget,\n parseFailure,\n });\n return stdout.replace(/\\n$/, '');\n } finally {\n removeTempFile(file);\n }\n}\n\n/**\n * Escape text for the inside of an AppleScript double-quoted literal.\n * AppleScript strings never interpolate, so escaping the quote, the backslash\n * and line breaks is the whole job.\n */\nexport function asEscape(text: string): string {\n return text\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\"/g, '\\\\\"')\n .replace(/\\r\\n|\\r|\\n/g, '\\\\n');\n}\n\n/** Text as an AppleScript string literal. */\nexport function asString(text: string): string {\n return `\"${asEscape(text)}\"`;\n}\n\n/** AppleScript's boolean literals. */\nexport function asBool(value: boolean): string {\n return value ? 'true' : 'false';\n}\n\n/** A whole number for a script; anything else is refused before it becomes code. */\nexport function asInt(value: number): string {\n if (!Number.isSafeInteger(value)) {\n throw new InvalidRequestError(`Expected a whole number, got ${String(value)}.`);\n }\n return String(value);\n}\n\n/** Message ids (already validated as integers) as an AppleScript list literal. */\nexport function asIdList(ids: readonly string[]): string {\n return `{${ids.map(id => asInt(Number(id))).join(', ')}}`;\n}\n\n// ── Record framing ───────────────────────────────────────────────────────\n//\n// Control characters separate fields, records and list items: subjects and\n// bodies contain tabs, newlines and commas, but never these — and every emitted\n// field passes through the `sanitize` handler, which makes the framing safe\n// rather than merely unlikely to break.\nexport const FIELD_SEP = '\u001f';\nexport const RECORD_SEP = '\u001e';\nexport const LIST_SEP = '\u001d';\n\n/** The same three, as AppleScript expressions. */\nexport const AS_FIELD_SEP = '(character id 31)';\nexport const AS_RECORD_SEP = '(character id 30)';\nexport const AS_LIST_SEP = '(character id 29)';\n\n/**\n * One record: the AppleScript expressions in `fields`, separated and terminated\n * so `splitRecords`/`splitFields` can take them apart again. A field holding a\n * LIST must be built with `my sanitizeList(...)`, which cleans the elements —\n * `sanitize` deliberately leaves the list separator alone.\n */\nexport function asRow(fields: readonly string[]): string {\n return fields\n .map(f => `my sanitize(${f})`)\n .join(` & ${AS_FIELD_SEP} & `) + ` & ${AS_RECORD_SEP}`;\n}\n\n/**\n * The handlers every script relies on, called with `my` from inside a `tell`.\n *\n * `isoDate` produces the zero-padded 'yyyy-MM-dd HH:mm' both platforms return;\n * `sanitize` strips the framing characters; `joinList` and `replaceText` are the\n * text-item-delimiter dance nobody should write twice.\n */\nexport const AS_HANDLERS = `\non pad2(n)\n set s to (n as integer) as string\n if (length of s) < 2 then set s to \"0\" & s\n return s\nend pad2\n\non isoDate(d)\n if d is missing value then return \"\"\n return (year of d as string) & \"-\" & pad2(month of d as integer) & \"-\" & pad2(day of d) & \" \" & pad2(hours of d) & \":\" & pad2(minutes of d)\nend isoDate\n\non joinList(lst, sep)\n set saved to AppleScript's text item delimiters\n set AppleScript's text item delimiters to sep\n set out to lst as string\n set AppleScript's text item delimiters to saved\n return out\nend joinList\n\non replaceText(t, findWhat, replaceWith)\n set saved to AppleScript's text item delimiters\n set AppleScript's text item delimiters to findWhat\n set parts to text items of t\n set AppleScript's text item delimiters to replaceWith\n set out to parts as string\n set AppleScript's text item delimiters to saved\n return out\nend replaceText\n\n-- Strips the FIELD and RECORD separators only. The LIST separator is left\n-- alone on purpose: a field may legitimately BE a list.\non sanitize(v)\n if v is missing value then return \"\"\n set t to v as string\n repeat with codePoint in {30, 31}\n set marker to (character id (codePoint as integer))\n if t contains marker then set t to my replaceText(t, marker, \" \")\n end repeat\n return t\nend sanitize\n\n-- A list field: each element loses all three separators, then they are joined\n-- with the LIST separator that splitList() takes them apart on.\non sanitizeList(lst)\n set cleaned to {}\n repeat with item_ in lst\n set t to my sanitize(item_)\n set marker to (character id 29)\n if t contains marker then set t to my replaceText(t, marker, \" \")\n set end of cleaned to t\n end repeat\n return my joinList(cleaned, (character id 29))\nend sanitizeList\n\non insertAboveQuoted(c, ins)\n if c is missing value then return ins\n set c to c as string\n if (length of c) is 0 then return ins\n -- A reply's own empty paragraph sits inside WordSection1; inserting straight\n -- after that tag puts the new text where the cursor would be. Anything else\n -- falls back to just inside <body>. ('at' is a reserved parameter name, so\n -- the cut point can't be called that.)\n set cutPoint to my tagEnd(c, \"${WORD_SECTION_ANCHOR}\")\n if cutPoint is 0 then set cutPoint to my tagEnd(c, \"<body\")\n if cutPoint is 0 then return ins & c\n if cutPoint is (length of c) then return c & ins\n return (text 1 thru cutPoint of c) & ins & (text (cutPoint + 1) thru -1 of c)\nend insertAboveQuoted\n\n-- 'rest' is an AppleScript term, so the remainder can't be called that:\n-- assigning to it compiles and then fails at run time.\non tagEnd(c, marker)\n set ix to offset of marker in c\n if ix is 0 then return 0\n if marker ends with \">\" then return ix + (length of marker) - 1\n set tailText to text ix thru -1 of c\n set gt to offset of \">\" in tailText\n if gt is 0 then return 0\n return ix + gt - 1\nend tagEnd\n`;\n\n/** A script's output as records, without the trailing empty one. */\nexport function splitRecords(raw: string): string[] {\n return raw.split(RECORD_SEP).filter(record => record.trim() !== '');\n}\n\n/** One record's fields. */\nexport function splitFields(record: string): string[] {\n return record.split(FIELD_SEP);\n}\n\n/** A field that carries a list. */\nexport function splitList(value: string | undefined): string[] {\n return (value || '').split(LIST_SEP).filter(Boolean);\n}\n\n/** A field by position; a missing field reads as ''. */\nexport function field(fields: readonly string[], index: number): string {\n return fields[index] ?? '';\n}\n\n/** The fields of a script's first record — the summary a mutation returns ahead of any per-item rows. */\nexport function summaryFields(raw: string): string[] {\n return splitFields(splitRecords(raw)[0] ?? '');\n}\n\n/** A numeric field, `fallback` when absent or unparsable. */\nexport function intField(fields: readonly string[], index: number, fallback = 0): number {\n const parsed = Number.parseInt(field(fields, index), 10);\n return Number.isNaN(parsed) ? fallback : parsed;\n}\n\n/** A boolean field; AppleScript renders booleans as the bare words true/false. */\nexport function boolField(fields: readonly string[], index: number): boolean {\n return field(fields, index).trim() === 'true';\n}\n"]}
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/mac/run.ts"],"names":[],"mappings":"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","sourcesContent":["// Getting a generated AppleScript to osascript and its output back.\n//\n// The macOS counterpart of windows/run.ts: running a script, the escaping\n// rules, and the record framing every reader here shares.\nimport fs from 'fs';\nimport { removeTempFile, type RunBudget, tempFile } from '../runtime';\nimport { runScriptProcess } from '../shared/exec';\nimport { InvalidRequestError } from '../errors';\nimport { WORD_SECTION_ANCHOR } from '../shared/replyBody';\nimport type { ItemFailure } from '../types';\n\nconst RUNNER = 'osascript' as const;\n\n/**\n * osascript's report of a script error: \"<file>:<start>:<end>: execution error:\n * <message> (<number>)\". Only the message is worth showing — these reach people\n * in per-item failure lists — and -2700 is the number AppleScript gives every\n * `error \"…\"` a script raises itself.\n */\nfunction parseFailure(stderr: string): { message: string } {\n const message = stderr\n .replace(/^(?:.*?:)?\\d+:\\d+:\\s*execution error:\\s*/m, '')\n .replace(/\\s*\\(-2700\\)\\s*$/, '')\n .trim();\n return { message };\n}\n\n/**\n * Run a script from a temp file (no argument-length or quoting limits) and\n * return what it printed, minus the newline osascript appends.\n *\n * `AS_HANDLERS` is prepended here rather than by each caller: a script missing\n * a handler fails to compile as a WHOLE, with an error pointing nowhere near the\n * omission, so it can't be left to each script to remember.\n */\nexport async function runOsaScript(source: string, budget: RunBudget): Promise<string> {\n // The full text is what osascript compiles, so it is also what error offsets\n // refer to and what the debug hook and errors carry.\n const script = AS_HANDLERS + source;\n const file = tempFile('osa', 'applescript');\n fs.writeFileSync(file, script, 'utf8');\n try {\n const { stdout } = await runScriptProcess({\n runner: RUNNER,\n command: 'osascript',\n args: [file],\n script,\n budget,\n parseFailure,\n });\n return stdout.replace(/\\n$/, '');\n } finally {\n removeTempFile(file);\n }\n}\n\n/**\n * Escape text for the inside of an AppleScript double-quoted literal.\n * AppleScript strings never interpolate, so escaping the quote, the backslash\n * and line breaks is the whole job.\n */\nexport function asEscape(text: string): string {\n return text\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\"/g, '\\\\\"')\n .replace(/\\r\\n|\\r|\\n/g, '\\\\n');\n}\n\n/** Text as an AppleScript string literal. */\nexport function asString(text: string): string {\n return `\"${asEscape(text)}\"`;\n}\n\n/** AppleScript's boolean literals. */\nexport function asBool(value: boolean): string {\n return value ? 'true' : 'false';\n}\n\n/** A whole number for a script; anything else is refused before it becomes code. */\nexport function asInt(value: number): string {\n if (!Number.isSafeInteger(value)) {\n throw new InvalidRequestError(`Expected a whole number, got ${String(value)}.`);\n }\n return String(value);\n}\n\n/** Message ids (already validated as integers) as an AppleScript list literal. */\nexport function asIdList(ids: readonly string[]): string {\n return `{${ids.map(id => asInt(Number(id))).join(', ')}}`;\n}\n\n// ── Record framing ───────────────────────────────────────────────────────\n//\n// Control characters separate fields, records and list items: subjects and\n// bodies contain tabs, newlines and commas, but never these — and every emitted\n// field passes through the `sanitize` handler, which makes the framing safe\n// rather than merely unlikely to break.\nexport const FIELD_SEP = '\u001f';\nexport const RECORD_SEP = '\u001e';\nexport const LIST_SEP = '\u001d';\n\n/** The same three, as AppleScript expressions. */\nexport const AS_FIELD_SEP = '(character id 31)';\nexport const AS_RECORD_SEP = '(character id 30)';\nexport const AS_LIST_SEP = '(character id 29)';\n\n/**\n * One record: the AppleScript expressions in `fields`, separated and terminated\n * so `splitRecords`/`splitFields` can take them apart again. A field holding a\n * LIST must be built with `my sanitizeList(...)`, which cleans the elements —\n * `sanitize` deliberately leaves the list separator alone.\n */\nexport function asRow(fields: readonly string[]): string {\n return fields\n .map(f => `my sanitize(${f})`)\n .join(` & ${AS_FIELD_SEP} & `) + ` & ${AS_RECORD_SEP}`;\n}\n\n/**\n * The handlers every script relies on, called with `my` from inside a `tell`.\n *\n * `isoDate` produces the zero-padded 'yyyy-MM-dd HH:mm' both platforms return;\n * `sanitize` strips the framing characters; `joinList` and `replaceText` are the\n * text-item-delimiter dance nobody should write twice.\n */\nexport const AS_HANDLERS = `\non pad2(n)\n set s to (n as integer) as string\n if (length of s) < 2 then set s to \"0\" & s\n return s\nend pad2\n\non isoDate(d)\n if d is missing value then return \"\"\n return (year of d as string) & \"-\" & pad2(month of d as integer) & \"-\" & pad2(day of d) & \" \" & pad2(hours of d) & \":\" & pad2(minutes of d)\nend isoDate\n\non joinList(lst, sep)\n set saved to AppleScript's text item delimiters\n set AppleScript's text item delimiters to sep\n set out to lst as string\n set AppleScript's text item delimiters to saved\n return out\nend joinList\n\non replaceText(t, findWhat, replaceWith)\n set saved to AppleScript's text item delimiters\n set AppleScript's text item delimiters to findWhat\n set parts to text items of t\n set AppleScript's text item delimiters to replaceWith\n set out to parts as string\n set AppleScript's text item delimiters to saved\n return out\nend replaceText\n\n-- Strips the FIELD and RECORD separators only. The LIST separator is left\n-- alone on purpose: a field may legitimately BE a list.\non sanitize(v)\n if v is missing value then return \"\"\n set t to v as string\n repeat with codePoint in {30, 31}\n set marker to (character id (codePoint as integer))\n if t contains marker then set t to my replaceText(t, marker, \" \")\n end repeat\n return t\nend sanitize\n\n-- A list field: each element loses all three separators, then they are joined\n-- with the LIST separator that splitList() takes them apart on.\non sanitizeList(lst)\n set cleaned to {}\n repeat with item_ in lst\n set t to my sanitize(item_)\n set marker to (character id 29)\n if t contains marker then set t to my replaceText(t, marker, \" \")\n set end of cleaned to t\n end repeat\n return my joinList(cleaned, (character id 29))\nend sanitizeList\n\n-- A list whose positions matter, like an email's attachments: every element is\n-- FOLLOWED by the LIST separator, so an empty element keeps its place and an\n-- empty list stays distinct from a list of one empty name. splitPositionalList()\n-- takes it apart.\non sanitizePositionalList(lst)\n if (count of lst) is 0 then return \"\"\n return my sanitizeList(lst) & (character id 29)\nend sanitizePositionalList\n\non insertAboveQuoted(c, ins)\n if c is missing value then return ins\n set c to c as string\n if (length of c) is 0 then return ins\n -- A reply's own empty paragraph sits inside WordSection1; inserting straight\n -- after that tag puts the new text where the cursor would be. Anything else\n -- falls back to just inside <body>. ('at' is a reserved parameter name, so\n -- the cut point can't be called that.)\n set cutPoint to my tagEnd(c, \"${WORD_SECTION_ANCHOR}\")\n if cutPoint is 0 then set cutPoint to my tagEnd(c, \"<body\")\n if cutPoint is 0 then return ins & c\n if cutPoint is (length of c) then return c & ins\n return (text 1 thru cutPoint of c) & ins & (text (cutPoint + 1) thru -1 of c)\nend insertAboveQuoted\n\n-- 'rest' is an AppleScript term, so the remainder can't be called that:\n-- assigning to it compiles and then fails at run time.\non tagEnd(c, marker)\n set ix to offset of marker in c\n if ix is 0 then return 0\n if marker ends with \">\" then return ix + (length of marker) - 1\n set tailText to text ix thru -1 of c\n set gt to offset of \">\" in tailText\n if gt is 0 then return 0\n return ix + gt - 1\nend tagEnd\n`;\n\n/** A script's output as records, without the trailing empty one. */\nexport function splitRecords(raw: string): string[] {\n return raw.split(RECORD_SEP).filter(record => record.trim() !== '');\n}\n\n/** One record's fields. */\nexport function splitFields(record: string): string[] {\n return record.split(FIELD_SEP);\n}\n\n/**\n * A field that carries a list. Empty elements are dropped, so where an index\n * into the list must match Outlook's, the field is written with\n * `sanitizePositionalList` and read with `splitPositionalList` instead.\n */\nexport function splitList(value: string | undefined): string[] {\n return (value || '').split(LIST_SEP).filter(Boolean);\n}\n\n/**\n * A field written by `sanitizePositionalList`: every element at its position,\n * empty ones included, so index N here is item N+1 of the AppleScript list.\n */\nexport function splitPositionalList(value: string | undefined): string[] {\n return value ? value.split(LIST_SEP).slice(0, -1) : [];\n}\n\n/** A field by position; a missing field reads as ''. */\nexport function field(fields: readonly string[], index: number): string {\n return fields[index] ?? '';\n}\n\n/** The fields of a script's first record — the summary a mutation returns ahead of any per-item rows. */\nexport function summaryFields(raw: string): string[] {\n return splitFields(splitRecords(raw)[0] ?? '');\n}\n\n/** A numeric field, `fallback` when absent or unparsable. */\nexport function intField(fields: readonly string[], index: number, fallback = 0): number {\n const parsed = Number.parseInt(field(fields, index), 10);\n return Number.isNaN(parsed) ? fallback : parsed;\n}\n\n/** A boolean field; AppleScript renders booleans as the bare words true/false. */\nexport function boolField(fields: readonly string[], index: number): boolean {\n return field(fields, index).trim() === 'true';\n}\n\n// ── Batches ──────────────────────────────────────────────────────────────\n//\n// A script acting on a list of ids returns one summary record, then one record\n// per id that failed. The failure row and its decoder sit together so the two\n// can't come to disagree about the field order.\n\n/** The record a batch script appends, inside `on error errText`, for the id `theId` it failed on. */\nexport const ITEM_FAILURE_ROW = asRow(['(theId as string)', 'subj', 'errText']);\n\n/** A batch script's output: the summary record's fields, and the items that failed. */\nexport function splitBatch(raw: string): { summary: string[]; failed: ItemFailure[] } {\n const [summary = '', ...failures] = splitRecords(raw);\n return {\n summary: splitFields(summary),\n failed: failures.map(record => {\n const parts = splitFields(record);\n return { entryId: field(parts, 0), subject: field(parts, 1), error: field(parts, 2) };\n }),\n };\n}\n"]}
|