@dawnlit/outlook-bridge 2.0.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 (99) hide show
  1. package/README.md +16 -9
  2. package/dist/backend.d.ts +2 -2
  3. package/dist/backend.js.map +1 -1
  4. package/dist/bridge.js +3 -3
  5. package/dist/bridge.js.map +1 -1
  6. package/dist/errors.d.ts +17 -8
  7. package/dist/errors.js +17 -13
  8. package/dist/errors.js.map +1 -1
  9. package/dist/index.d.ts +3 -3
  10. package/dist/index.js +3 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/mac/accounts.d.ts +4 -0
  13. package/dist/mac/accounts.js +10 -0
  14. package/dist/mac/accounts.js.map +1 -1
  15. package/dist/mac/attachments.js +1 -1
  16. package/dist/mac/attachments.js.map +1 -1
  17. package/dist/mac/bounces.js +18 -31
  18. package/dist/mac/bounces.js.map +1 -1
  19. package/dist/mac/cleanup.d.ts +11 -3
  20. package/dist/mac/cleanup.js +30 -58
  21. package/dist/mac/cleanup.js.map +1 -1
  22. package/dist/mac/drafts.d.ts +3 -1
  23. package/dist/mac/drafts.js +37 -49
  24. package/dist/mac/drafts.js.map +1 -1
  25. package/dist/mac/folders.d.ts +10 -3
  26. package/dist/mac/folders.js +31 -23
  27. package/dist/mac/folders.js.map +1 -1
  28. package/dist/mac/profile.d.ts +12 -0
  29. package/dist/mac/profile.js +75 -1
  30. package/dist/mac/profile.js.map +1 -1
  31. package/dist/mac/read.d.ts +4 -0
  32. package/dist/mac/read.js +68 -42
  33. package/dist/mac/read.js.map +1 -1
  34. package/dist/mac/run.d.ts +19 -5
  35. package/dist/mac/run.js +49 -9
  36. package/dist/mac/run.js.map +1 -1
  37. package/dist/mac/scripts.d.ts +67 -14
  38. package/dist/mac/scripts.js +150 -31
  39. package/dist/mac/scripts.js.map +1 -1
  40. package/dist/mac/send.d.ts +5 -4
  41. package/dist/mac/send.js +52 -14
  42. package/dist/mac/send.js.map +1 -1
  43. package/dist/mac/templates.js +16 -22
  44. package/dist/mac/templates.js.map +1 -1
  45. package/dist/mail.d.ts +11 -3
  46. package/dist/mail.js +25 -6
  47. package/dist/mail.js.map +1 -1
  48. package/dist/runtime.js.map +1 -1
  49. package/dist/shared/args.js.map +1 -1
  50. package/dist/shared/attachmentMatch.d.ts +0 -6
  51. package/dist/shared/attachmentMatch.js +0 -1
  52. package/dist/shared/attachmentMatch.js.map +1 -1
  53. package/dist/shared/exec.js.map +1 -1
  54. package/dist/shared/locate.d.ts +0 -8
  55. package/dist/shared/locate.js +0 -1
  56. package/dist/shared/locate.js.map +1 -1
  57. package/dist/shared/replyBody.d.ts +0 -15
  58. package/dist/shared/replyBody.js +0 -2
  59. package/dist/shared/replyBody.js.map +1 -1
  60. package/dist/shared/templateLimits.d.ts +14 -0
  61. package/dist/shared/templateLimits.js +21 -0
  62. package/dist/shared/templateLimits.js.map +1 -0
  63. package/dist/templateBody.d.ts +6 -0
  64. package/dist/templateBody.js +8 -1
  65. package/dist/templateBody.js.map +1 -1
  66. package/dist/types.d.ts +43 -7
  67. package/dist/types.js.map +1 -1
  68. package/dist/windows/accounts.js +1 -1
  69. package/dist/windows/accounts.js.map +1 -1
  70. package/dist/windows/attachments.js +2 -2
  71. package/dist/windows/attachments.js.map +1 -1
  72. package/dist/windows/bounces.js +7 -7
  73. package/dist/windows/bounces.js.map +1 -1
  74. package/dist/windows/cleanup.d.ts +5 -1
  75. package/dist/windows/cleanup.js +18 -15
  76. package/dist/windows/cleanup.js.map +1 -1
  77. package/dist/windows/drafts.d.ts +4 -4
  78. package/dist/windows/drafts.js +22 -33
  79. package/dist/windows/drafts.js.map +1 -1
  80. package/dist/windows/folders.js +2 -2
  81. package/dist/windows/folders.js.map +1 -1
  82. package/dist/{shared → windows}/json.d.ts +11 -6
  83. package/dist/{shared → windows}/json.js +12 -7
  84. package/dist/windows/json.js.map +1 -0
  85. package/dist/windows/read.js +23 -16
  86. package/dist/windows/read.js.map +1 -1
  87. package/dist/windows/run.d.ts +0 -2
  88. package/dist/windows/run.js +1 -2
  89. package/dist/windows/run.js.map +1 -1
  90. package/dist/windows/scripts.d.ts +10 -4
  91. package/dist/windows/scripts.js +21 -12
  92. package/dist/windows/scripts.js.map +1 -1
  93. package/dist/windows/send.d.ts +13 -4
  94. package/dist/windows/send.js +24 -6
  95. package/dist/windows/send.js.map +1 -1
  96. package/dist/windows/templates.js +11 -17
  97. package/dist/windows/templates.js.map +1 -1
  98. package/package.json +2 -2
  99. package/dist/shared/json.js.map +0 -1
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.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
@@ -106,12 +108,12 @@ function asIdList(ids) {
106
108
  // bodies contain tabs, newlines and commas, but never these — and every emitted
107
109
  // field passes through the `sanitize` handler, which makes the framing safe
108
110
  // rather than merely unlikely to break.
109
- exports.FIELD_SEP = '';
110
- exports.RECORD_SEP = '';
111
- exports.LIST_SEP = '';
112
- /** The same three, as AppleScript expressions. */
113
- exports.AS_FIELD_SEP = '(character id 31)';
114
- exports.AS_RECORD_SEP = '(character id 30)';
111
+ exports.FIELD_SEP = '\x1f';
112
+ exports.RECORD_SEP = '\x1e';
113
+ exports.LIST_SEP = '\x1d';
114
+ /** The same three, as AppleScript expressions. Only the list one is written outside this module. */
115
+ const AS_FIELD_SEP = '(character id 31)';
116
+ const AS_RECORD_SEP = '(character id 30)';
115
117
  exports.AS_LIST_SEP = '(character id 29)';
116
118
  /**
117
119
  * One record: the AppleScript expressions in `fields`, separated and terminated
@@ -122,7 +124,7 @@ exports.AS_LIST_SEP = '(character id 29)';
122
124
  function asRow(fields) {
123
125
  return fields
124
126
  .map(f => `my sanitize(${f})`)
125
- .join(` & ${exports.AS_FIELD_SEP} & `) + ` & ${exports.AS_RECORD_SEP}`;
127
+ .join(` & ${AS_FIELD_SEP} & `) + ` & ${AS_RECORD_SEP}`;
126
128
  }
127
129
  /**
128
130
  * The handlers every script relies on, called with `my` from inside a `tell`.
@@ -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
- /** A field that carries a list. */
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
@@ -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 = '\\x1f';\nexport const RECORD_SEP = '\\x1e';\nexport const LIST_SEP = '\\x1d';\n\n/** The same three, as AppleScript expressions. Only the list one is written outside this module. */\nconst AS_FIELD_SEP = '(character id 31)';\nconst 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"]}
@@ -12,6 +12,12 @@ import type { ItemFailure, MailFolderRef } from '../types';
12
12
  export interface MacAccount {
13
13
  readonly emailAccount: string;
14
14
  readonly folderIds?: Readonly<Record<string, number>>;
15
+ /**
16
+ * Set when the profile has no row for the address because macOS refused it:
17
+ * the sentence to fail with should AppleScript not find the account either,
18
+ * since the profile is where a mailbox AppleScript can't see would have been.
19
+ */
20
+ readonly profileRefusal?: string;
15
21
  }
16
22
  /** Look an address up in Outlook's profile database, for the folder ids it can add. */
17
23
  export declare function resolveMacAccount(emailAccount: string): Promise<MacAccount>;
@@ -52,8 +58,43 @@ export declare function dateProperty(rootId: number): string;
52
58
  * AppleScript literal is unreadable, and the parts are known here anyway.
53
59
  */
54
60
  export declare function macFolderPath(emailAccount: string, rootLabel: string, segments: readonly string[]): string;
55
- /** The dictionary term for a root, or NOT_IMPLEMENTED naming what was asked. */
56
- export declare function rootTerm(ref: MailFolderRef, folderLabel?: string): string;
61
+ /**
62
+ * Walk a folder's `container` chain to the top as the list `chainNames`,
63
+ * nearest first. Requires `theFolder`, and leaves `cur` on the folder the walk
64
+ * stopped at: the account's nameless root folder, when it got that far.
65
+ *
66
+ * The chain's last element is the folder directly under the account, and an
67
+ * account can't hold two top-level folders of one name, so comparing that name
68
+ * is exact.
69
+ */
70
+ export declare function folderChainSnippet(indent: string): string;
71
+ /**
72
+ * Bind `acctRootId` to the id of the account's root folder — the folder every
73
+ * one of its top-level folders sits in — through the account object when the
74
+ * probe found one, else from the profile database. Requires
75
+ * `accountLookupSnippet`.
76
+ *
77
+ * Never fails: an account neither names a root for leaves `acctRootId` as
78
+ * `missing value`, which `ownMailGuardSnippet` reads as a root nothing can be
79
+ * shown to sit under.
80
+ */
81
+ export declare function accountRootIdSnippet(acct: MacAccount): string;
82
+ /**
83
+ * Fail unless `theMsg` is the account's own mail — the macOS counterpart of
84
+ * Windows resolving an id in the account's store. `message id N` resolves
85
+ * against the application, so an id alone says nothing about whose mailbox the
86
+ * message is in, and a stale or wrong one can name mail in another.
87
+ *
88
+ * Either of two signs is enough, because neither exists for every mailbox:
89
+ * - the message's folder chain ends at the account's root folder. Requires
90
+ * `folderChainSnippet` (for `cur`) and `accountRootIdSnippet`.
91
+ * - the message's own account carries the address. A mailbox the dictionary
92
+ * publishes no account object for answers this with `missing value` (see
93
+ * profile.ts), which is why the first sign has to be able to stand alone.
94
+ * An item neither vouches for is refused rather than given the benefit of the
95
+ * doubt: what follows moves or deletes it.
96
+ */
97
+ export declare function ownMailGuardSnippet(emailAccount: string, verb: 'delete' | 'move', indent: string): string;
57
98
  /**
58
99
  * Resolve `scopeFolder` from `targetAcct`: a well-known root walked down through
59
100
  * any further segments. Also binds `scopeCreated`.
@@ -81,16 +122,28 @@ export declare function partitionMessageIds(entryIds: readonly string[]): {
81
122
  invalid: ItemFailure[];
82
123
  };
83
124
  /**
84
- * Resolve `variable` from a message id, or fail as NOT_FOUND. `message id N`
85
- * resolves against the application, so the message is found wherever it sits.
125
+ * Bind `theMsg` to a message id, or fail as NOT_FOUND. `message id N` resolves
126
+ * against the application, so the message is found wherever it sits.
127
+ */
128
+ export declare function messageLookupSnippet(id: string): string;
129
+ /**
130
+ * Bind `theMsg` to the id a batch loop holds in `theId`, leaving it `missing
131
+ * value` when the id no longer resolves — the per-item counterpart of
132
+ * `messageLookupSnippet`, for a loop that skips or reports a miss and goes on
133
+ * rather than failing the whole run.
134
+ */
135
+ export declare function messageByIdSnippet(indent: string): string;
136
+ /**
137
+ * `theMsg`'s plain-text body as `bodyText`: whole, cut to `maxChars`, or — with
138
+ * a `maxChars` of 0 — not read at all, the body being the one expensive
139
+ * property of a message.
86
140
  */
87
- export declare function messageLookupSnippet(id: string, variable?: string): string;
141
+ export declare function bodyTextSnippet(indent: string, maxChars?: number): string;
88
142
  /**
89
- * The sender's display name and address as `sndName` / `sndAddr`. `sender` is a
90
- * record, and `address of sender of m` doesn't coerce, so the record is bound
91
- * first.
143
+ * `theMsg`'s sender as `sndName` / `sndAddr`. `sender` is a record, and
144
+ * `address of sender of m` doesn't coerce, so the record is bound first.
92
145
  */
93
- export declare function senderSnippet(messageVariable?: string, indent?: string): string;
146
+ export declare function senderSnippet(indent?: string): string;
94
147
  /**
95
148
  * Everything a reader wants off one message bound to `theMsg`, emitted together
96
149
  * with the row it produces (see `messageDetailFields`) so the read order and the
@@ -111,12 +164,12 @@ export declare const MessageDetail: {
111
164
  readonly body: 6;
112
165
  };
113
166
  /**
114
- * The first recipient's name and address as `sndName` / `sndAddr` — what
115
- * outgoing mail reports in place of a sender.
167
+ * `theMsg`'s first recipient as `sndName` / `sndAddr` — what outgoing mail
168
+ * reports in place of a sender.
116
169
  */
117
- export declare function firstRecipientSnippet(messageVariable?: string, indent?: string): string;
118
- /** Every address a message was sent to — To, CC and BCC — as the list `addrList`. */
119
- export declare function allRecipientsSnippet(messageVariable?: string, indent?: string): string;
170
+ export declare function firstRecipientSnippet(indent?: string): string;
171
+ /** Every address `theMsg` was sent to — To, CC and BCC — as the list `addrList`. */
172
+ export declare function allRecipientsSnippet(indent?: string): string;
120
173
  /**
121
174
  * Recursive, case-insensitive, depth-capped folder search from `startFolder` —
122
175
  * the macOS counterpart of the Windows Find-FolderByName.
@@ -6,11 +6,15 @@ exports.accountLookupSnippet = accountLookupSnippet;
6
6
  exports.rootFolderSnippet = rootFolderSnippet;
7
7
  exports.dateProperty = dateProperty;
8
8
  exports.macFolderPath = macFolderPath;
9
- exports.rootTerm = rootTerm;
9
+ exports.folderChainSnippet = folderChainSnippet;
10
+ exports.accountRootIdSnippet = accountRootIdSnippet;
11
+ exports.ownMailGuardSnippet = ownMailGuardSnippet;
10
12
  exports.mailScopeSnippet = mailScopeSnippet;
11
13
  exports.macMessageId = macMessageId;
12
14
  exports.partitionMessageIds = partitionMessageIds;
13
15
  exports.messageLookupSnippet = messageLookupSnippet;
16
+ exports.messageByIdSnippet = messageByIdSnippet;
17
+ exports.bodyTextSnippet = bodyTextSnippet;
14
18
  exports.senderSnippet = senderSnippet;
15
19
  exports.messageDetailSnippet = messageDetailSnippet;
16
20
  exports.messageDetailFields = messageDetailFields;
@@ -26,7 +30,12 @@ async function resolveMacAccount(emailAccount) {
26
30
  const wanted = emailAccount.trim().toLowerCase();
27
31
  const found = (await (0, profile_1.readProfileAccounts)())
28
32
  .find(account => account.emailAccount.trim().toLowerCase() === wanted);
29
- return found ? { emailAccount, folderIds: found.folderIds } : { emailAccount };
33
+ if (found)
34
+ return { emailAccount, folderIds: found.folderIds };
35
+ // Asked only for an address the profile did not give: the script still looks
36
+ // for an account object first, so a classic account works without access.
37
+ const refusal = await (0, profile_1.profileRefusal)();
38
+ return refusal ? { emailAccount, profileRefusal: refusal } : { emailAccount };
30
39
  }
31
40
  /**
32
41
  * Resolve the account whose address matches into `targetAcct`, inside a
@@ -54,10 +63,18 @@ function accountLookupSnippet(acct, needsAccountObject = false) {
54
63
  end try`;
55
64
  if (acct.folderIds && !needsAccountObject)
56
65
  return probe;
57
- const complaint = acct.folderIds
58
- ? `${(0, errors_1.failureTag)('INVALID_REQUEST')}Outlook publishes no account object for '${acct.emailAccount}', `
59
- + 'so the account a message is sent from cannot be set. Its folders can be read; mail cannot be composed from it.'
60
- : `${(0, errors_1.failureTag)('ACCOUNT_NOT_FOUND')}Account '${acct.emailAccount}' not found in Outlook.`;
66
+ let complaint;
67
+ if (acct.folderIds) {
68
+ complaint = `${(0, errors_1.failureTag)('INVALID_REQUEST')}Outlook publishes no account object for '${acct.emailAccount}', `
69
+ + 'so the account a message is sent from cannot be set. Its folders can be read; mail cannot be composed from it.';
70
+ }
71
+ else if (acct.profileRefusal) {
72
+ complaint = `${(0, errors_1.failureTag)('PERMISSION_DENIED')}Account '${acct.emailAccount}' is not one Outlook's scripting `
73
+ + `can see. ${acct.profileRefusal}`;
74
+ }
75
+ else {
76
+ complaint = `${(0, errors_1.failureTag)('ACCOUNT_NOT_FOUND')}Account '${acct.emailAccount}' not found in Outlook.`;
77
+ }
61
78
  return `${probe}
62
79
  if targetAcct is missing value then error ${(0, run_1.asString)(complaint)}`;
63
80
  }
@@ -113,6 +130,84 @@ function dateProperty(rootId) {
113
130
  function macFolderPath(emailAccount, rootLabel, segments) {
114
131
  return ['\\\\' + emailAccount, rootLabel, ...segments].join('\\');
115
132
  }
133
+ /**
134
+ * Walk a folder's `container` chain to the top as the list `chainNames`,
135
+ * nearest first. Requires `theFolder`, and leaves `cur` on the folder the walk
136
+ * stopped at: the account's nameless root folder, when it got that far.
137
+ *
138
+ * The chain's last element is the folder directly under the account, and an
139
+ * account can't hold two top-level folders of one name, so comparing that name
140
+ * is exact.
141
+ */
142
+ function folderChainSnippet(indent) {
143
+ return `${indent}set chainNames to {}
144
+ ${indent}set cur to theFolder
145
+ ${indent}set guard to 0
146
+ ${indent}repeat while cur is not missing value and guard < 25
147
+ ${indent} -- The account's root folder has a name of missing value, and coercing
148
+ ${indent} -- that yields the STRING "missing value" — so test before coercing.
149
+ ${indent} set rawName to missing value
150
+ ${indent} try
151
+ ${indent} set rawName to name of cur
152
+ ${indent} end try
153
+ ${indent} if rawName is missing value then exit repeat
154
+ ${indent} set nm to rawName as string
155
+ ${indent} if nm is "" then exit repeat
156
+ ${indent} set end of chainNames to nm
157
+ ${indent} set nextUp to missing value
158
+ ${indent} try
159
+ ${indent} set nextUp to container of cur
160
+ ${indent} end try
161
+ ${indent} set cur to nextUp
162
+ ${indent} set guard to guard + 1
163
+ ${indent}end repeat`;
164
+ }
165
+ /**
166
+ * Bind `acctRootId` to the id of the account's root folder — the folder every
167
+ * one of its top-level folders sits in — through the account object when the
168
+ * probe found one, else from the profile database. Requires
169
+ * `accountLookupSnippet`.
170
+ *
171
+ * Never fails: an account neither names a root for leaves `acctRootId` as
172
+ * `missing value`, which `ownMailGuardSnippet` reads as a root nothing can be
173
+ * shown to sit under.
174
+ */
175
+ function accountRootIdSnippet(acct) {
176
+ const profiled = acct.folderIds?.['root folder'];
177
+ return ` set acctRootId to missing value
178
+ try
179
+ if targetAcct is not missing value then set acctRootId to id of (root folder of targetAcct)
180
+ end try` + (profiled === undefined ? '' : `
181
+ if acctRootId is missing value then set acctRootId to ${(0, run_1.asInt)(profiled)}`);
182
+ }
183
+ /**
184
+ * Fail unless `theMsg` is the account's own mail — the macOS counterpart of
185
+ * Windows resolving an id in the account's store. `message id N` resolves
186
+ * against the application, so an id alone says nothing about whose mailbox the
187
+ * message is in, and a stale or wrong one can name mail in another.
188
+ *
189
+ * Either of two signs is enough, because neither exists for every mailbox:
190
+ * - the message's folder chain ends at the account's root folder. Requires
191
+ * `folderChainSnippet` (for `cur`) and `accountRootIdSnippet`.
192
+ * - the message's own account carries the address. A mailbox the dictionary
193
+ * publishes no account object for answers this with `missing value` (see
194
+ * profile.ts), which is why the first sign has to be able to stand alone.
195
+ * An item neither vouches for is refused rather than given the benefit of the
196
+ * doubt: what follows moves or deletes it.
197
+ */
198
+ function ownMailGuardSnippet(emailAccount, verb, indent) {
199
+ return `${indent}set inAccount to false
200
+ ${indent}try
201
+ ${indent} if acctRootId is not missing value and (id of cur) is acctRootId then set inAccount to true
202
+ ${indent}end try
203
+ ${indent}if not inAccount then
204
+ ${indent} try
205
+ ${indent} set ownerAddr to email address of (account of theMsg)
206
+ ${indent} if ownerAddr is not missing value and (ownerAddr as string) is ${(0, run_1.asString)(emailAccount)} then set inAccount to true
207
+ ${indent} end try
208
+ ${indent}end if
209
+ ${indent}if not inAccount then error "item is in another mailbox, not this account - refusing to ${verb}"`;
210
+ }
116
211
  /** The dictionary term for a root, or NOT_IMPLEMENTED naming what was asked. */
117
212
  function rootTerm(ref, folderLabel = '') {
118
213
  const term = exports.MAC_ROOT_TERMS[ref.rootId];
@@ -188,27 +283,55 @@ function partitionMessageIds(entryIds) {
188
283
  return { valid, invalid };
189
284
  }
190
285
  /**
191
- * Resolve `variable` from a message id, or fail as NOT_FOUND. `message id N`
192
- * resolves against the application, so the message is found wherever it sits.
286
+ * Bind `theMsg` to a message id, or fail as NOT_FOUND. `message id N` resolves
287
+ * against the application, so the message is found wherever it sits.
193
288
  */
194
- function messageLookupSnippet(id, variable = 'theMsg') {
289
+ function messageLookupSnippet(id) {
195
290
  const complaint = `${(0, errors_1.failureTag)('NOT_FOUND', 'email')}No email found for message id '${id}'. It may have been deleted - list the mail again for a current id.`;
196
- return ` set ${variable} to missing value
291
+ return ` set theMsg to missing value
197
292
  try
198
- set ${variable} to message id ${id}
293
+ set theMsg to message id ${id}
199
294
  end try
200
- if ${variable} is missing value then error ${(0, run_1.asString)(complaint)}`;
295
+ if theMsg is missing value then error ${(0, run_1.asString)(complaint)}`;
201
296
  }
202
297
  /**
203
- * The sender's display name and address as `sndName` / `sndAddr`. `sender` is a
204
- * record, and `address of sender of m` doesn't coerce, so the record is bound
205
- * first.
298
+ * Bind `theMsg` to the id a batch loop holds in `theId`, leaving it `missing
299
+ * value` when the id no longer resolves — the per-item counterpart of
300
+ * `messageLookupSnippet`, for a loop that skips or reports a miss and goes on
301
+ * rather than failing the whole run.
206
302
  */
207
- function senderSnippet(messageVariable = 'theMsg', indent = ' ') {
303
+ function messageByIdSnippet(indent) {
304
+ return `${indent}set theMsg to missing value
305
+ ${indent}try
306
+ ${indent} set theMsg to message id theId
307
+ ${indent}end try`;
308
+ }
309
+ /**
310
+ * `theMsg`'s plain-text body as `bodyText`: whole, cut to `maxChars`, or — with
311
+ * a `maxChars` of 0 — not read at all, the body being the one expensive
312
+ * property of a message.
313
+ */
314
+ function bodyTextSnippet(indent, maxChars) {
315
+ if (maxChars === 0)
316
+ return `${indent}set bodyText to ""`;
317
+ const read = `${indent}set bodyText to ""
318
+ ${indent}try
319
+ ${indent} set bodyText to (plain text content of theMsg) as string
320
+ ${indent}end try`;
321
+ if (maxChars === undefined)
322
+ return read;
323
+ return `${read}
324
+ ${indent}if (length of bodyText) > ${(0, run_1.asInt)(maxChars)} then set bodyText to text 1 thru ${(0, run_1.asInt)(maxChars)} of bodyText`;
325
+ }
326
+ /**
327
+ * `theMsg`'s sender as `sndName` / `sndAddr`. `sender` is a record, and
328
+ * `address of sender of m` doesn't coerce, so the record is bound first.
329
+ */
330
+ function senderSnippet(indent = ' ') {
208
331
  return `${indent}set sndName to ""
209
332
  ${indent}set sndAddr to ""
210
333
  ${indent}try
211
- ${indent} set snd to sender of ${messageVariable}
334
+ ${indent} set snd to sender of theMsg
212
335
  ${indent} try
213
336
  ${indent} set sndAddr to (address of snd) as string
214
337
  ${indent} end try
@@ -228,7 +351,7 @@ function messageDetailSnippet(withBody = false, indent = ' ') {
228
351
  ${indent}try
229
352
  ${indent} set subj to (subject of theMsg) as string
230
353
  ${indent}end try
231
- ${senderSnippet('theMsg', indent)}
354
+ ${senderSnippet(indent)}
232
355
  ${indent}set recvd to ""
233
356
  ${indent}try
234
357
  ${indent} set recvd to my isoDate(time received of theMsg)
@@ -240,15 +363,11 @@ ${indent}end try
240
363
  ${indent}set attNames to {}
241
364
  ${indent}try
242
365
  ${indent} set attNames to name of every attachment of theMsg
243
- ${indent}end try` + (withBody ? `
244
- ${indent}set bodyText to ""
245
- ${indent}try
246
- ${indent} set bodyText to (plain text content of theMsg) as string
247
- ${indent}end try` : '');
366
+ ${indent}end try` + (withBody ? `\n${bodyTextSnippet(indent)}` : '');
248
367
  }
249
368
  /** The fields `messageDetailSnippet` fills, in row order — see `MessageDetail`. */
250
369
  function messageDetailFields(withBody = false) {
251
- const fields = ['(id of theMsg as string)', 'subj', 'sndName', 'sndAddr', 'recvd', 'my sanitizeList(attNames)'];
370
+ const fields = ['(id of theMsg as string)', 'subj', 'sndName', 'sndAddr', 'recvd', 'my sanitizePositionalList(attNames)'];
252
371
  if (withBody)
253
372
  fields.push('bodyText');
254
373
  return fields;
@@ -264,14 +383,14 @@ exports.MessageDetail = {
264
383
  body: 6,
265
384
  };
266
385
  /**
267
- * The first recipient's name and address as `sndName` / `sndAddr` — what
268
- * outgoing mail reports in place of a sender.
386
+ * `theMsg`'s first recipient as `sndName` / `sndAddr` — what outgoing mail
387
+ * reports in place of a sender.
269
388
  */
270
- function firstRecipientSnippet(messageVariable = 'theMsg', indent = ' ') {
389
+ function firstRecipientSnippet(indent = ' ') {
271
390
  return `${indent}set sndName to ""
272
391
  ${indent}set sndAddr to ""
273
392
  ${indent}try
274
- ${indent} set rcps to to recipients of ${messageVariable}
393
+ ${indent} set rcps to to recipients of theMsg
275
394
  ${indent} if (count of rcps) > 0 then
276
395
  ${indent} set ea to email address of (item 1 of rcps)
277
396
  ${indent} try
@@ -283,11 +402,11 @@ ${indent} end try
283
402
  ${indent} end if
284
403
  ${indent}end try`;
285
404
  }
286
- /** Every address a message was sent to — To, CC and BCC — as the list `addrList`. */
287
- function allRecipientsSnippet(messageVariable = 'theMsg', indent = ' ') {
405
+ /** Every address `theMsg` was sent to — To, CC and BCC — as the list `addrList`. */
406
+ function allRecipientsSnippet(indent = ' ') {
288
407
  return `${indent}set addrList to {}
289
408
  ${indent}try
290
- ${indent} repeat with r in (every recipient of ${messageVariable})
409
+ ${indent} repeat with r in (every recipient of theMsg)
291
410
  ${indent} try
292
411
  ${indent} -- The record is bound before its field is read: a nested
293
412
  ${indent} -- 'address of (email address of r)' doesn't coerce, and fails