@dawnlit/outlook-bridge 1.1.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +150 -183
  2. package/dist/backend.d.ts +151 -0
  3. package/dist/backend.js +3 -0
  4. package/dist/backend.js.map +1 -0
  5. package/dist/bridge.d.ts +56 -0
  6. package/dist/bridge.js +507 -0
  7. package/dist/bridge.js.map +1 -0
  8. package/dist/errors.d.ts +81 -43
  9. package/dist/errors.js +98 -52
  10. package/dist/errors.js.map +1 -1
  11. package/dist/index.d.ts +10 -10
  12. package/dist/index.js +25 -52
  13. package/dist/index.js.map +1 -1
  14. package/dist/mac/accounts.d.ts +7 -2
  15. package/dist/mac/accounts.js +23 -6
  16. package/dist/mac/accounts.js.map +1 -1
  17. package/dist/mac/attachments.d.ts +5 -25
  18. package/dist/mac/attachments.js +40 -99
  19. package/dist/mac/attachments.js.map +1 -1
  20. package/dist/mac/bounces.d.ts +12 -27
  21. package/dist/mac/bounces.js +95 -112
  22. package/dist/mac/bounces.js.map +1 -1
  23. package/dist/mac/cleanup.d.ts +11 -25
  24. package/dist/mac/cleanup.js +50 -76
  25. package/dist/mac/cleanup.js.map +1 -1
  26. package/dist/mac/drafts.d.ts +13 -34
  27. package/dist/mac/drafts.js +95 -169
  28. package/dist/mac/drafts.js.map +1 -1
  29. package/dist/mac/folders.d.ts +10 -21
  30. package/dist/mac/folders.js +33 -47
  31. package/dist/mac/folders.js.map +1 -1
  32. package/dist/mac/index.d.ts +2 -22
  33. package/dist/mac/index.js +19 -81
  34. package/dist/mac/index.js.map +1 -1
  35. package/dist/mac/profile.d.ts +13 -0
  36. package/dist/mac/profile.js +149 -0
  37. package/dist/mac/profile.js.map +1 -0
  38. package/dist/mac/read.d.ts +18 -38
  39. package/dist/mac/read.js +154 -210
  40. package/dist/mac/read.js.map +1 -1
  41. package/dist/mac/run.d.ts +35 -39
  42. package/dist/mac/run.js +97 -102
  43. package/dist/mac/run.js.map +1 -1
  44. package/dist/mac/scripts.d.ts +78 -86
  45. package/dist/mac/scripts.js +125 -115
  46. package/dist/mac/scripts.js.map +1 -1
  47. package/dist/mac/send.d.ts +10 -20
  48. package/dist/mac/send.js +64 -86
  49. package/dist/mac/send.js.map +1 -1
  50. package/dist/mac/signatures.d.ts +2 -2
  51. package/dist/mac/signatures.js +9 -11
  52. package/dist/mac/signatures.js.map +1 -1
  53. package/dist/mac/templates.d.ts +14 -26
  54. package/dist/mac/templates.js +75 -113
  55. package/dist/mac/templates.js.map +1 -1
  56. package/dist/mail.d.ts +77 -45
  57. package/dist/mail.js +166 -73
  58. package/dist/mail.js.map +1 -1
  59. package/dist/runtime.d.ts +76 -43
  60. package/dist/runtime.js +103 -48
  61. package/dist/runtime.js.map +1 -1
  62. package/dist/shared/args.d.ts +48 -0
  63. package/dist/shared/args.js +216 -0
  64. package/dist/shared/args.js.map +1 -0
  65. package/dist/shared/attachmentMatch.d.ts +28 -16
  66. package/dist/shared/attachmentMatch.js +60 -22
  67. package/dist/shared/attachmentMatch.js.map +1 -1
  68. package/dist/shared/bounceReport.d.ts +11 -0
  69. package/dist/shared/bounceReport.js +45 -0
  70. package/dist/shared/bounceReport.js.map +1 -0
  71. package/dist/shared/bounceRules.d.ts +38 -10
  72. package/dist/shared/bounceRules.js +50 -32
  73. package/dist/shared/bounceRules.js.map +1 -1
  74. package/dist/shared/exec.d.ts +21 -0
  75. package/dist/shared/exec.js +47 -0
  76. package/dist/shared/exec.js.map +1 -0
  77. package/dist/shared/json.d.ts +12 -6
  78. package/dist/shared/json.js +29 -23
  79. package/dist/shared/json.js.map +1 -1
  80. package/dist/shared/locate.d.ts +39 -0
  81. package/dist/shared/locate.js +129 -0
  82. package/dist/shared/locate.js.map +1 -0
  83. package/dist/shared/replyBody.d.ts +42 -32
  84. package/dist/shared/replyBody.js +84 -71
  85. package/dist/shared/replyBody.js.map +1 -1
  86. package/dist/templateBody.d.ts +63 -0
  87. package/dist/{outlookTemplateSections.js → templateBody.js} +72 -72
  88. package/dist/templateBody.js.map +1 -0
  89. package/dist/types.d.ts +360 -167
  90. package/dist/types.js +8 -9
  91. package/dist/types.js.map +1 -1
  92. package/dist/windows/accounts.d.ts +7 -12
  93. package/dist/windows/accounts.js +19 -34
  94. package/dist/windows/accounts.js.map +1 -1
  95. package/dist/windows/attachments.d.ts +9 -24
  96. package/dist/windows/attachments.js +94 -119
  97. package/dist/windows/attachments.js.map +1 -1
  98. package/dist/windows/bounces.d.ts +13 -26
  99. package/dist/windows/bounces.js +168 -200
  100. package/dist/windows/bounces.js.map +1 -1
  101. package/dist/windows/cleanup.d.ts +19 -23
  102. package/dist/windows/cleanup.js +92 -111
  103. package/dist/windows/cleanup.js.map +1 -1
  104. package/dist/windows/drafts.d.ts +16 -35
  105. package/dist/windows/drafts.js +167 -253
  106. package/dist/windows/drafts.js.map +1 -1
  107. package/dist/windows/folders.d.ts +8 -13
  108. package/dist/windows/folders.js +44 -52
  109. package/dist/windows/folders.js.map +1 -1
  110. package/dist/windows/index.d.ts +2 -27
  111. package/dist/windows/index.js +11 -66
  112. package/dist/windows/index.js.map +1 -1
  113. package/dist/windows/read.d.ts +24 -60
  114. package/dist/windows/read.js +261 -306
  115. package/dist/windows/read.js.map +1 -1
  116. package/dist/windows/run.d.ts +39 -39
  117. package/dist/windows/run.js +151 -111
  118. package/dist/windows/run.js.map +1 -1
  119. package/dist/windows/scripts.d.ts +65 -55
  120. package/dist/windows/scripts.js +109 -85
  121. package/dist/windows/scripts.js.map +1 -1
  122. package/dist/windows/send.d.ts +6 -11
  123. package/dist/windows/send.js +68 -88
  124. package/dist/windows/send.js.map +1 -1
  125. package/dist/windows/signatures.d.ts +4 -10
  126. package/dist/windows/signatures.js +30 -37
  127. package/dist/windows/signatures.js.map +1 -1
  128. package/dist/windows/templates.d.ts +12 -18
  129. package/dist/windows/templates.js +122 -149
  130. package/dist/windows/templates.js.map +1 -1
  131. package/package.json +4 -3
  132. package/dist/OutlookService.d.ts +0 -43
  133. package/dist/OutlookService.js +0 -137
  134. package/dist/OutlookService.js.map +0 -1
  135. package/dist/outlookTemplateSections.d.ts +0 -62
  136. package/dist/outlookTemplateSections.js.map +0 -1
  137. package/dist/shared/capabilities.d.ts +0 -3
  138. package/dist/shared/capabilities.js +0 -9
  139. package/dist/shared/capabilities.js.map +0 -1
@@ -1,30 +1,35 @@
1
1
  "use strict";
2
- // Matching an attachment by name, and the error when nothing matches.
3
- //
4
- // Both are shared because the error is the useful half. Resolving an email by id
5
- // happily returns a DIFFERENT message when the id is stale or wrong — common
6
- // precisely when many replies share one subject — so a bare "not found" points
7
- // the caller at the wrong problem. Naming the email actually resolved, and the
8
- // attachments it does carry, is what makes the mistake visible.
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
9
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MAX_FILE_STEM = exports.RESERVED_FILE_NAME = exports.UNSAFE_FILE_NAME_CHARS = void 0;
10
7
  exports.normalizeAttachmentName = normalizeAttachmentName;
11
8
  exports.findAttachmentIndex = findAttachmentIndex;
12
9
  exports.attachmentNotFoundMessage = attachmentNotFoundMessage;
10
+ exports.safeFileName = safeFileName;
11
+ exports.uniqueSavePath = uniqueSavePath;
12
+ // Matching an attachment by name, the error when nothing matches, and the
13
+ // file name it is saved under.
14
+ //
15
+ // The error is the useful half. Resolving an email by id can return a
16
+ // DIFFERENT message when the id is stale or wrong — common exactly when many
17
+ // replies share one subject — so a bare "not found" points at the wrong
18
+ // problem. Naming the email that actually resolved, and the attachments it does
19
+ // carry, is what makes the mistake visible.
20
+ const path_1 = __importDefault(require("path"));
13
21
  /**
14
22
  * Collapse whitespace runs (non-breaking spaces included) and trim, so a
15
- * trivially reformatted filename still resolves. Both platforms' regex engines
16
- * treat `\s` as covering U+00A0, which is the character a mail client is most
17
- * likely to have substituted.
23
+ * trivially reformatted file name still resolves. Both platforms' regex engines
24
+ * count U+00A0 as whitespace.
18
25
  */
19
26
  function normalizeAttachmentName(name) {
20
27
  return name.replace(/\s+/g, ' ').trim();
21
28
  }
22
29
  /**
23
- * Index of the attachment matching `wanted`, or -1.
24
- *
25
- * Exact first, then whitespace-normalized and case-insensitive — in that order,
26
- * so an email carrying two names differing only in case still resolves the one
27
- * that was actually asked for.
30
+ * Index of the attachment matching `wanted`, or -1: exact first, then
31
+ * whitespace-normalized and case-insensitive — in that order, so two names that
32
+ * differ only in case still resolve the one asked for.
28
33
  */
29
34
  function findAttachmentIndex(names, wanted) {
30
35
  const exact = names.indexOf(wanted);
@@ -34,17 +39,50 @@ function findAttachmentIndex(names, wanted) {
34
39
  return names.findIndex(name => normalizeAttachmentName(name).toLowerCase() === normalized);
35
40
  }
36
41
  /**
37
- * The "attachment not found" message, worded once for both platforms.
38
- *
39
- * Every field arrives already stringified, which is what lets the Windows script
40
- * build the identical sentence: it passes PowerShell interpolations
41
- * (`$($item.Subject)`) where macOS passes the values it read.
42
+ * The "attachment not found" message, worded once for both platforms. Fields
43
+ * arrive as strings so the Windows script can build the same sentence from
44
+ * PowerShell variables where macOS passes the values it read.
42
45
  */
43
46
  function attachmentNotFoundMessage(init) {
44
47
  return `Attachment '${init.fileName}' not found. `
45
48
  + `Resolved email: from=${init.senderEmail}; subject=${init.subject}; received=${init.receivedTime}. `
46
49
  + `Attachments present: ${init.present}. `
47
- + `If this is not the email you expected, the ${init.idLabel} is likely wrong or stale - `
48
- + `re-run readInboxEmails to get a current ${init.idLabel}.`;
50
+ + 'If this is not the email you expected, its id is likely wrong or stale - list the mail again for a current one.';
51
+ }
52
+ /** Characters no file name may carry on Windows (a superset of macOS's). */
53
+ exports.UNSAFE_FILE_NAME_CHARS = '[<>:"/\\\\|?*\\x00-\\x1F]';
54
+ /** Names Windows reserves for devices, with or without an extension. */
55
+ exports.RESERVED_FILE_NAME = '^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\\..*)?$';
56
+ /** How long a saved name's stem may run before it is cut. */
57
+ exports.MAX_FILE_STEM = 150;
58
+ /**
59
+ * A sender-chosen attachment name made safe to write: no path separators or
60
+ * characters a file system rejects, no trailing dots or spaces, no reserved
61
+ * device name, and a stem short enough to stay clear of path-length limits. A
62
+ * name like `..\..\x.exe` therefore lands inside the destination as
63
+ * `.._.._x.exe`, never outside it.
64
+ */
65
+ function safeFileName(name) {
66
+ let safe = name.replace(new RegExp(exports.UNSAFE_FILE_NAME_CHARS, 'g'), '_').replace(/[. ]+$/, '');
67
+ if (new RegExp(exports.RESERVED_FILE_NAME, 'i').test(safe))
68
+ safe = `_${safe}`;
69
+ const ext = path_1.default.extname(safe);
70
+ const stem = safe.slice(0, safe.length - ext.length);
71
+ if (stem.length > exports.MAX_FILE_STEM)
72
+ safe = stem.slice(0, exports.MAX_FILE_STEM) + ext;
73
+ return safe || 'attachment';
74
+ }
75
+ /**
76
+ * A path in `dir` for `name` that `exists` reports free: the name itself, else
77
+ * `name (1).ext`, `name (2).ext`, … Existing files are never overwritten.
78
+ */
79
+ function uniqueSavePath(dir, name, exists) {
80
+ const ext = path_1.default.extname(name);
81
+ const stem = name.slice(0, name.length - ext.length);
82
+ let candidate = path_1.default.join(dir, name);
83
+ for (let n = 1; exists(candidate); n++) {
84
+ candidate = path_1.default.join(dir, `${stem} (${n})${ext}`);
85
+ }
86
+ return candidate;
49
87
  }
50
88
  //# sourceMappingURL=attachmentMatch.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"attachmentMatch.js","sourceRoot":"","sources":["../../src/shared/attachmentMatch.ts"],"names":[],"mappings":";AAAA,sEAAsE;AACtE,EAAE;AACF,iFAAiF;AACjF,6EAA6E;AAC7E,+EAA+E;AAC/E,+EAA+E;AAC/E,gEAAgE;;AAQhE,0DAEC;AASD,kDAKC;AASD,8DAeC;AA9CD;;;;;GAKG;AACH,SAAgB,uBAAuB,CAAC,IAAY;IAChD,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AAC5C,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,mBAAmB,CAAC,KAAwB,EAAE,MAAc;IACxE,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,KAAK,KAAK,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAC/B,MAAM,UAAU,GAAG,uBAAuB,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;IACjE,OAAO,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,uBAAuB,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,KAAK,UAAU,CAAC,CAAC;AAC/F,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,yBAAyB,CAAC,IASzC;IACG,OAAO,eAAe,IAAI,CAAC,QAAQ,eAAe;UAC5C,wBAAwB,IAAI,CAAC,WAAW,aAAa,IAAI,CAAC,OAAO,cAAc,IAAI,CAAC,YAAY,IAAI;UACpG,wBAAwB,IAAI,CAAC,OAAO,IAAI;UACxC,8CAA8C,IAAI,CAAC,OAAO,8BAA8B;UACxF,2CAA2C,IAAI,CAAC,OAAO,GAAG,CAAC;AACrE,CAAC"}
1
+ {"version":3,"file":"attachmentMatch.js","sourceRoot":"","sources":["../../src/shared/attachmentMatch.ts"],"names":[],"mappings":";;;;;;AAeA,0DAEC;AAOD,kDAKC;AAOD,8DAYC;AAkBD,oCAOC;AAMD,wCAQC;AAvFD,0EAA0E;AAC1E,+BAA+B;AAC/B,EAAE;AACF,sEAAsE;AACtE,6EAA6E;AAC7E,wEAAwE;AACxE,gFAAgF;AAChF,4CAA4C;AAC5C,gDAAwB;AAExB;;;;GAIG;AACH,SAAgB,uBAAuB,CAAC,IAAY;IAChD,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AAC5C,CAAC;AAED;;;;GAIG;AACH,SAAgB,mBAAmB,CAAC,KAAwB,EAAE,MAAc;IACxE,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,KAAK,KAAK,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAC/B,MAAM,UAAU,GAAG,uBAAuB,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;IACjE,OAAO,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,uBAAuB,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,KAAK,UAAU,CAAC,CAAC;AAC/F,CAAC;AAED;;;;GAIG;AACH,SAAgB,yBAAyB,CAAC,IAOzC;IACG,OAAO,eAAe,IAAI,CAAC,QAAQ,eAAe;UAC5C,wBAAwB,IAAI,CAAC,WAAW,aAAa,IAAI,CAAC,OAAO,cAAc,IAAI,CAAC,YAAY,IAAI;UACpG,wBAAwB,IAAI,CAAC,OAAO,IAAI;UACxC,iHAAiH,CAAC;AAC5H,CAAC;AAED,4EAA4E;AAC/D,QAAA,sBAAsB,GAAG,2BAA2B,CAAC;AAElE,wEAAwE;AAC3D,QAAA,kBAAkB,GAAG,+CAA+C,CAAC;AAElF,6DAA6D;AAChD,QAAA,aAAa,GAAG,GAAG,CAAC;AAEjC;;;;;;GAMG;AACH,SAAgB,YAAY,CAAC,IAAY;IACrC,IAAI,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,8BAAsB,EAAE,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAC5F,IAAI,IAAI,MAAM,CAAC,0BAAkB,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;IACtE,MAAM,GAAG,GAAG,cAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;IACrD,IAAI,IAAI,CAAC,MAAM,GAAG,qBAAa;QAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,qBAAa,CAAC,GAAG,GAAG,CAAC;IAC3E,OAAO,IAAI,IAAI,YAAY,CAAC;AAChC,CAAC;AAED;;;GAGG;AACH,SAAgB,cAAc,CAAC,GAAW,EAAE,IAAY,EAAE,MAAiC;IACvF,MAAM,GAAG,GAAG,cAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;IACrD,IAAI,SAAS,GAAG,cAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACrC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,SAAS,GAAG,cAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,KAAK,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,SAAS,CAAC;AACrB,CAAC","sourcesContent":["// Matching an attachment by name, the error when nothing matches, and the\n// file name it is saved under.\n//\n// The error is the useful half. Resolving an email by id can return a\n// DIFFERENT message when the id is stale or wrong — common exactly when many\n// replies share one subject — so a bare \"not found\" points at the wrong\n// problem. Naming the email that actually resolved, and the attachments it does\n// carry, is what makes the mistake visible.\nimport path from 'path';\n\n/**\n * Collapse whitespace runs (non-breaking spaces included) and trim, so a\n * trivially reformatted file name still resolves. Both platforms' regex engines\n * count U+00A0 as whitespace.\n */\nexport function normalizeAttachmentName(name: string): string {\n return name.replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * Index of the attachment matching `wanted`, or -1: exact first, then\n * whitespace-normalized and case-insensitive — in that order, so two names that\n * differ only in case still resolve the one asked for.\n */\nexport function findAttachmentIndex(names: readonly string[], wanted: string): number {\n const exact = names.indexOf(wanted);\n if (exact !== -1) return exact;\n const normalized = normalizeAttachmentName(wanted).toLowerCase();\n return names.findIndex(name => normalizeAttachmentName(name).toLowerCase() === normalized);\n}\n\n/**\n * The \"attachment not found\" message, worded once for both platforms. Fields\n * arrive as strings so the Windows script can build the same sentence from\n * PowerShell variables where macOS passes the values it read.\n */\nexport function attachmentNotFoundMessage(init: {\n fileName: string;\n senderEmail: string;\n subject: string;\n receivedTime: string;\n /** The attachments the email does carry, joined for display. */\n present: string;\n}): string {\n return `Attachment '${init.fileName}' not found. `\n + `Resolved email: from=${init.senderEmail}; subject=${init.subject}; received=${init.receivedTime}. `\n + `Attachments present: ${init.present}. `\n + 'If this is not the email you expected, its id is likely wrong or stale - list the mail again for a current one.';\n}\n\n/** Characters no file name may carry on Windows (a superset of macOS's). */\nexport const UNSAFE_FILE_NAME_CHARS = '[<>:\"/\\\\\\\\|?*\\\\x00-\\\\x1F]';\n\n/** Names Windows reserves for devices, with or without an extension. */\nexport const RESERVED_FILE_NAME = '^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\\\\..*)?$';\n\n/** How long a saved name's stem may run before it is cut. */\nexport const MAX_FILE_STEM = 150;\n\n/**\n * A sender-chosen attachment name made safe to write: no path separators or\n * characters a file system rejects, no trailing dots or spaces, no reserved\n * device name, and a stem short enough to stay clear of path-length limits. A\n * name like `..\\..\\x.exe` therefore lands inside the destination as\n * `.._.._x.exe`, never outside it.\n */\nexport function safeFileName(name: string): string {\n let safe = name.replace(new RegExp(UNSAFE_FILE_NAME_CHARS, 'g'), '_').replace(/[. ]+$/, '');\n if (new RegExp(RESERVED_FILE_NAME, 'i').test(safe)) safe = `_${safe}`;\n const ext = path.extname(safe);\n const stem = safe.slice(0, safe.length - ext.length);\n if (stem.length > MAX_FILE_STEM) safe = stem.slice(0, MAX_FILE_STEM) + ext;\n return safe || 'attachment';\n}\n\n/**\n * A path in `dir` for `name` that `exists` reports free: the name itself, else\n * `name (1).ext`, `name (2).ext`, … Existing files are never overwritten.\n */\nexport function uniqueSavePath(dir: string, name: string, exists: (file: string) => boolean): string {\n const ext = path.extname(name);\n const stem = name.slice(0, name.length - ext.length);\n let candidate = path.join(dir, name);\n for (let n = 1; exists(candidate); n++) {\n candidate = path.join(dir, `${stem} (${n})${ext}`);\n }\n return candidate;\n}\n"]}
@@ -0,0 +1,11 @@
1
+ import type { BounceReport, SentRecipientGroup } from '../types';
2
+ /**
3
+ * Every sent message with a bounced recipient, newest first, and the bounced
4
+ * addresses no sent message accounts for.
5
+ *
6
+ * Kept per message rather than per address or domain: a message's recipient
7
+ * set is the one grouping a mailbox actually records, and whether a failure
8
+ * means anything — one colleague gone, or nobody reached at all — depends on
9
+ * what else that same message went to.
10
+ */
11
+ export declare function buildBounceReport(account: string, scannedDays: number, bouncedAddresses: readonly string[], sent: readonly SentRecipientGroup[]): BounceReport;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildBounceReport = buildBounceReport;
4
+ function distinctLowerCase(addresses) {
5
+ return [...new Set(addresses.map(address => address.trim().toLowerCase()).filter(Boolean))];
6
+ }
7
+ /**
8
+ * Every sent message with a bounced recipient, newest first, and the bounced
9
+ * addresses no sent message accounts for.
10
+ *
11
+ * Kept per message rather than per address or domain: a message's recipient
12
+ * set is the one grouping a mailbox actually records, and whether a failure
13
+ * means anything — one colleague gone, or nobody reached at all — depends on
14
+ * what else that same message went to.
15
+ */
16
+ function buildBounceReport(account, scannedDays, bouncedAddresses, sent) {
17
+ const bounced = distinctLowerCase(bouncedAddresses);
18
+ const bouncedSet = new Set(bounced);
19
+ const matched = new Set();
20
+ const sends = [];
21
+ const newestFirst = [...sent].sort((a, b) => b.sentOn.localeCompare(a.sentOn));
22
+ for (const message of newestFirst) {
23
+ const recipients = distinctLowerCase(message.recipients);
24
+ const failedRecipients = recipients.filter(address => bouncedSet.has(address));
25
+ if (failedRecipients.length === 0)
26
+ continue;
27
+ failedRecipients.forEach(address => matched.add(address));
28
+ sends.push({
29
+ entryId: message.entryId,
30
+ subject: message.subject,
31
+ sentOn: message.sentOn,
32
+ recipients,
33
+ failedRecipients,
34
+ allFailed: failedRecipients.length === recipients.length,
35
+ });
36
+ }
37
+ return {
38
+ account,
39
+ scannedDays,
40
+ bouncedAddresses: bounced,
41
+ sends,
42
+ unmatchedAddresses: bounced.filter(address => !matched.has(address)).sort(),
43
+ };
44
+ }
45
+ //# sourceMappingURL=bounceReport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bounceReport.js","sourceRoot":"","sources":["../../src/shared/bounceReport.ts"],"names":[],"mappings":";;AAkBA,8CAkCC;AA/CD,SAAS,iBAAiB,CAAC,SAA4B;IACnD,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAChG,CAAC;AAED;;;;;;;;GAQG;AACH,SAAgB,iBAAiB,CAC7B,OAAe,EACf,WAAmB,EACnB,gBAAmC,EACnC,IAAmC;IAEnC,MAAM,OAAO,GAAG,iBAAiB,CAAC,gBAAgB,CAAC,CAAC;IACpD,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IACpC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,MAAM,KAAK,GAAkB,EAAE,CAAC;IAEhC,MAAM,WAAW,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/E,KAAK,MAAM,OAAO,IAAI,WAAW,EAAE,CAAC;QAChC,MAAM,UAAU,GAAG,iBAAiB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACzD,MAAM,gBAAgB,GAAG,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;QAC/E,IAAI,gBAAgB,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAC5C,gBAAgB,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;QAC1D,KAAK,CAAC,IAAI,CAAC;YACP,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,UAAU;YACV,gBAAgB;YAChB,SAAS,EAAE,gBAAgB,CAAC,MAAM,KAAK,UAAU,CAAC,MAAM;SAC3D,CAAC,CAAC;IACP,CAAC;IAED,OAAO;QACH,OAAO;QACP,WAAW;QACX,gBAAgB,EAAE,OAAO;QACzB,KAAK;QACL,kBAAkB,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE;KAC9E,CAAC;AACN,CAAC","sourcesContent":["// Measuring what was sent against what bounced. Both reads are platform work\n// (collectBouncedRecipients, readSentRecipientGroups); putting them side by side\n// is the same everywhere, so it is done once here.\nimport type { BouncedSend, BounceReport, SentRecipientGroup } from '../types';\n\nfunction distinctLowerCase(addresses: readonly string[]): string[] {\n return [...new Set(addresses.map(address => address.trim().toLowerCase()).filter(Boolean))];\n}\n\n/**\n * Every sent message with a bounced recipient, newest first, and the bounced\n * addresses no sent message accounts for.\n *\n * Kept per message rather than per address or domain: a message's recipient\n * set is the one grouping a mailbox actually records, and whether a failure\n * means anything — one colleague gone, or nobody reached at all — depends on\n * what else that same message went to.\n */\nexport function buildBounceReport(\n account: string,\n scannedDays: number,\n bouncedAddresses: readonly string[],\n sent: readonly SentRecipientGroup[],\n): BounceReport {\n const bounced = distinctLowerCase(bouncedAddresses);\n const bouncedSet = new Set(bounced);\n const matched = new Set<string>();\n const sends: BouncedSend[] = [];\n\n const newestFirst = [...sent].sort((a, b) => b.sentOn.localeCompare(a.sentOn));\n for (const message of newestFirst) {\n const recipients = distinctLowerCase(message.recipients);\n const failedRecipients = recipients.filter(address => bouncedSet.has(address));\n if (failedRecipients.length === 0) continue;\n failedRecipients.forEach(address => matched.add(address));\n sends.push({\n entryId: message.entryId,\n subject: message.subject,\n sentOn: message.sentOn,\n recipients,\n failedRecipients,\n allFailed: failedRecipients.length === recipients.length,\n });\n }\n\n return {\n account,\n scannedDays,\n bouncedAddresses: bounced,\n sends,\n unmatchedAddresses: bounced.filter(address => !matched.has(address)).sort(),\n };\n}\n"]}
@@ -1,4 +1,4 @@
1
- /** Subject fragments that only a mail system writes. Matched case-insensitively. */
1
+ /** Subject fragments only a mail system writes. Matched case-insensitively. */
2
2
  export declare const BOUNCE_SUBJECT_PHRASES: readonly string[];
3
3
  /** Fragments of a sender ADDRESS that fingerprint a mail-delivery daemon. */
4
4
  export declare const BOUNCE_DAEMON_ADDRESSES: readonly string[];
@@ -6,28 +6,56 @@ export declare const BOUNCE_DAEMON_ADDRESSES: readonly string[];
6
6
  export declare const BOUNCE_DAEMON_NAMES: readonly string[];
7
7
  /** MessageClass prefix Exchange stamps on a non-delivery report. */
8
8
  export declare const NDR_MESSAGE_CLASS_PREFIX = "REPORT.IPM.Note.NDR";
9
+ /**
10
+ * The same structural signal as it arrives on the wire, for macOS: a bounce is
11
+ * a delivery-status notification (RFC 3464) and says so in its own headers.
12
+ * Both markers must be present — `multipart/report` also carries read receipts,
13
+ * and it is the report type that separates a delivery that FAILED from one that
14
+ * was acknowledged.
15
+ *
16
+ * Outlook for Mac publishes no MessageClass in its AppleScript dictionary, so
17
+ * this stands in for it: the message Exchange stamps REPORT.IPM.Note.NDR is the
18
+ * message that arrived carrying these headers.
19
+ *
20
+ * The macOS index tests these in AppleScript, whose `contains` is already
21
+ * case-insensitive, and returns one flag per message rather than the headers
22
+ * themselves (see mac/bounces.ts).
23
+ */
24
+ export declare const DSN_HEADER_MARKERS: readonly string[];
25
+ /** Address fragments never reported as a failed recipient — they are the bounce's own author. */
26
+ export declare const DAEMON_SELF_ADDRESSES: readonly string[];
27
+ /** At most this many failed recipients per bounce; a list bounce can name hundreds. */
28
+ export declare const MAX_FAILED_RECIPIENTS = 5;
29
+ /** The reasons a bounce is flagged, worded once. */
30
+ export declare const BOUNCE_REASON: {
31
+ readonly ndr: "Non-delivery report (NDR)";
32
+ readonly daemon: "From mail-delivery system";
33
+ readonly subjectPhrase: (phrase: string) => string;
34
+ };
9
35
  /**
10
36
  * Why this item is a bounce, or '' if it isn't.
11
37
  *
12
- * Order matches the Windows script's: the structural signal (an NDR message
13
- * class) outranks the sender fingerprint, which outranks the subject phrase —
14
- * so the reason a caller is shown is the strongest one that applied, not the
15
- * first one that happened to be tested.
38
+ * The structural signal (a non-delivery report) outranks the sender
39
+ * fingerprint, which outranks a subject phrase — so the reason reported is the
40
+ * strongest one that applied, not whichever was tested first.
16
41
  */
17
42
  export declare function bounceReason(item: {
18
43
  subject?: string;
19
44
  senderName?: string;
20
45
  senderEmail?: string;
21
- /** True when the item's MessageClass marks it a non-delivery report (Windows only). */
46
+ /**
47
+ * True when the item is STRUCTURALLY a non-delivery report, whichever way
48
+ * its platform can see that: its MessageClass on Windows, the
49
+ * delivery-status headers it arrived with on macOS.
50
+ */
22
51
  isNonDeliveryReport?: boolean;
23
52
  }): string;
24
53
  /** The address pattern both platforms scan a bounce body with. */
25
54
  export declare const BODY_ADDRESS_PATTERN = "[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}";
26
55
  /**
27
56
  * The addresses a bounce body names as having failed — best effort, since a
28
- * bounce body is prose with no agreed structure.
29
- *
30
- * The account's own address and the daemon's are dropped: both appear in nearly
31
- * every bounce, and neither is a send that failed.
57
+ * bounce body is prose with no agreed structure. The account's own address and
58
+ * the daemon's are dropped: both appear in nearly every bounce and neither is a
59
+ * send that failed.
32
60
  */
33
61
  export declare function failedRecipients(body: string, accountAddress: string): string[];
@@ -1,21 +1,21 @@
1
1
  "use strict";
2
2
  // What counts as a bounce, defined once for both platforms.
3
3
  //
4
- // Windows classifies inside the generated PowerShell (the item body is only
5
- // touched once a cheaper signal has matched, which is what keeps a large-inbox
6
- // scan affordable) while macOS classifies here in TypeScript after a bulk
7
- // property read. Two implementations of the RULES, though, is how "what is a
8
- // bounce" quietly comes to mean two different things — so the lists live here
9
- // and the PowerShell arrays are generated from them (see windows/bounces.ts).
4
+ // Windows classifies inside the generated PowerShell — an item's body is only
5
+ // read once a cheaper signal has matched, which keeps a large-inbox scan
6
+ // affordable — while macOS classifies here after a bulk property read. Two
7
+ // implementations of the RULES is how "what is a bounce" comes to mean two
8
+ // things, so every list and limit lives here and the PowerShell is generated
9
+ // from them (see windows/bounces.ts).
10
10
  //
11
- // Deliberately specific, multi-word, mail-system wording: ordinary mail that
12
- // merely mentions a "delivery" must never be flagged, because the caller's next
13
- // move is to delete what this matches.
11
+ // Deliberately specific, mail-system wording: ordinary mail that merely
12
+ // mentions a "delivery" must never match, because the next step is often to
13
+ // delete what did.
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.BODY_ADDRESS_PATTERN = exports.NDR_MESSAGE_CLASS_PREFIX = exports.BOUNCE_DAEMON_NAMES = exports.BOUNCE_DAEMON_ADDRESSES = exports.BOUNCE_SUBJECT_PHRASES = void 0;
15
+ exports.BODY_ADDRESS_PATTERN = exports.BOUNCE_REASON = exports.MAX_FAILED_RECIPIENTS = exports.DAEMON_SELF_ADDRESSES = exports.DSN_HEADER_MARKERS = exports.NDR_MESSAGE_CLASS_PREFIX = exports.BOUNCE_DAEMON_NAMES = exports.BOUNCE_DAEMON_ADDRESSES = exports.BOUNCE_SUBJECT_PHRASES = void 0;
16
16
  exports.bounceReason = bounceReason;
17
17
  exports.failedRecipients = failedRecipients;
18
- /** Subject fragments that only a mail system writes. Matched case-insensitively. */
18
+ /** Subject fragments only a mail system writes. Matched case-insensitively. */
19
19
  exports.BOUNCE_SUBJECT_PHRASES = [
20
20
  'undeliverable',
21
21
  'message blocked',
@@ -46,41 +46,59 @@ exports.BOUNCE_DAEMON_NAMES = [
46
46
  ];
47
47
  /** MessageClass prefix Exchange stamps on a non-delivery report. */
48
48
  exports.NDR_MESSAGE_CLASS_PREFIX = 'REPORT.IPM.Note.NDR';
49
- /** Addresses never reported as a failed recipient — they are the bounce's author. */
50
- const DAEMON_SELF_ADDRESSES = ['mailer-daemon', 'postmaster', 'mail-daemon'];
49
+ /**
50
+ * The same structural signal as it arrives on the wire, for macOS: a bounce is
51
+ * a delivery-status notification (RFC 3464) and says so in its own headers.
52
+ * Both markers must be present — `multipart/report` also carries read receipts,
53
+ * and it is the report type that separates a delivery that FAILED from one that
54
+ * was acknowledged.
55
+ *
56
+ * Outlook for Mac publishes no MessageClass in its AppleScript dictionary, so
57
+ * this stands in for it: the message Exchange stamps REPORT.IPM.Note.NDR is the
58
+ * message that arrived carrying these headers.
59
+ *
60
+ * The macOS index tests these in AppleScript, whose `contains` is already
61
+ * case-insensitive, and returns one flag per message rather than the headers
62
+ * themselves (see mac/bounces.ts).
63
+ */
64
+ exports.DSN_HEADER_MARKERS = ['multipart/report', 'delivery-status'];
65
+ /** Address fragments never reported as a failed recipient — they are the bounce's own author. */
66
+ exports.DAEMON_SELF_ADDRESSES = ['mailer-daemon', 'postmaster', 'mail-daemon'];
51
67
  /** At most this many failed recipients per bounce; a list bounce can name hundreds. */
52
- const MAX_FAILED_RECIPIENTS = 5;
68
+ exports.MAX_FAILED_RECIPIENTS = 5;
69
+ /** The reasons a bounce is flagged, worded once. */
70
+ exports.BOUNCE_REASON = {
71
+ ndr: 'Non-delivery report (NDR)',
72
+ daemon: 'From mail-delivery system',
73
+ subjectPhrase: (phrase) => `Bounce subject phrase: '${phrase}'`,
74
+ };
53
75
  /**
54
76
  * Why this item is a bounce, or '' if it isn't.
55
77
  *
56
- * Order matches the Windows script's: the structural signal (an NDR message
57
- * class) outranks the sender fingerprint, which outranks the subject phrase —
58
- * so the reason a caller is shown is the strongest one that applied, not the
59
- * first one that happened to be tested.
78
+ * The structural signal (a non-delivery report) outranks the sender
79
+ * fingerprint, which outranks a subject phrase — so the reason reported is the
80
+ * strongest one that applied, not whichever was tested first.
60
81
  */
61
82
  function bounceReason(item) {
62
83
  if (item.isNonDeliveryReport)
63
- return 'Non-delivery report (NDR)';
84
+ return exports.BOUNCE_REASON.ndr;
64
85
  const senderEmail = (item.senderEmail || '').toLowerCase();
65
- if (exports.BOUNCE_DAEMON_ADDRESSES.some(fragment => senderEmail.includes(fragment))) {
66
- return 'From mail-delivery system';
67
- }
86
+ if (exports.BOUNCE_DAEMON_ADDRESSES.some(fragment => senderEmail.includes(fragment)))
87
+ return exports.BOUNCE_REASON.daemon;
68
88
  const senderName = (item.senderName || '').toLowerCase();
69
- if (exports.BOUNCE_DAEMON_NAMES.some(fragment => senderName.includes(fragment))) {
70
- return 'From mail-delivery system';
71
- }
89
+ if (exports.BOUNCE_DAEMON_NAMES.some(fragment => senderName.includes(fragment)))
90
+ return exports.BOUNCE_REASON.daemon;
72
91
  const subject = (item.subject || '').toLowerCase();
73
92
  const phrase = exports.BOUNCE_SUBJECT_PHRASES.find(p => subject.includes(p));
74
- return phrase ? `Bounce subject phrase: '${phrase}'` : '';
93
+ return phrase ? exports.BOUNCE_REASON.subjectPhrase(phrase) : '';
75
94
  }
76
95
  /** The address pattern both platforms scan a bounce body with. */
77
96
  exports.BODY_ADDRESS_PATTERN = '[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}';
78
97
  /**
79
98
  * The addresses a bounce body names as having failed — best effort, since a
80
- * bounce body is prose with no agreed structure.
81
- *
82
- * The account's own address and the daemon's are dropped: both appear in nearly
83
- * every bounce, and neither is a send that failed.
99
+ * bounce body is prose with no agreed structure. The account's own address and
100
+ * the daemon's are dropped: both appear in nearly every bounce and neither is a
101
+ * send that failed.
84
102
  */
85
103
  function failedRecipients(body, accountAddress) {
86
104
  if (!body)
@@ -91,12 +109,12 @@ function failedRecipients(body, accountAddress) {
91
109
  const address = match[0].toLowerCase().replace(/\.+$/, '');
92
110
  if (address === target)
93
111
  continue;
94
- if (DAEMON_SELF_ADDRESSES.some(fragment => address.includes(fragment)))
112
+ if (exports.DAEMON_SELF_ADDRESSES.some(fragment => address.includes(fragment)))
95
113
  continue;
96
114
  if (found.includes(address))
97
115
  continue;
98
116
  found.push(address);
99
- if (found.length >= MAX_FAILED_RECIPIENTS)
117
+ if (found.length >= exports.MAX_FAILED_RECIPIENTS)
100
118
  break;
101
119
  }
102
120
  return found;
@@ -1 +1 @@
1
- {"version":3,"file":"bounceRules.js","sourceRoot":"","sources":["../../src/shared/bounceRules.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bounceRules.js","sourceRoot":"","sources":["../../src/shared/bounceRules.ts"],"names":[],"mappings":";AAAA,4DAA4D;AAC5D,EAAE;AACF,8EAA8E;AAC9E,yEAAyE;AACzE,2EAA2E;AAC3E,2EAA2E;AAC3E,6EAA6E;AAC7E,sCAAsC;AACtC,EAAE;AACF,wEAAwE;AACxE,4EAA4E;AAC5E,mBAAmB;;;AA0EnB,oCAoBC;AAWD,4CAaC;AApHD,+EAA+E;AAClE,QAAA,sBAAsB,GAAsB;IACrD,eAAe;IACf,iBAAiB;IACjB,sBAAsB;IACtB,wCAAwC;IACxC,qBAAqB;IACrB,gBAAgB;IAChB,qCAAqC;IACrC,eAAe;CAClB,CAAC;AAEF,6EAA6E;AAChE,QAAA,uBAAuB,GAAsB;IACtD,eAAe;IACf,eAAe;IACf,aAAa;IACb,aAAa;IACb,cAAc;CACjB,CAAC;AAEF,kFAAkF;AACrE,QAAA,mBAAmB,GAAsB;IAClD,yBAAyB;IACzB,sBAAsB;IACtB,mBAAmB;IACnB,YAAY;IACZ,eAAe;IACf,oBAAoB;IACpB,wBAAwB;CAC3B,CAAC;AAEF,oEAAoE;AACvD,QAAA,wBAAwB,GAAG,qBAAqB,CAAC;AAE9D;;;;;;;;;;;;;;GAcG;AACU,QAAA,kBAAkB,GAAsB,CAAC,kBAAkB,EAAE,iBAAiB,CAAC,CAAC;AAE7F,iGAAiG;AACpF,QAAA,qBAAqB,GAAsB,CAAC,eAAe,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC;AAEvG,uFAAuF;AAC1E,QAAA,qBAAqB,GAAG,CAAC,CAAC;AAEvC,oDAAoD;AACvC,QAAA,aAAa,GAAG;IACzB,GAAG,EAAE,2BAA2B;IAChC,MAAM,EAAE,2BAA2B;IACnC,aAAa,EAAE,CAAC,MAAc,EAAE,EAAE,CAAC,2BAA2B,MAAM,GAAG;CACjE,CAAC;AAEX;;;;;;GAMG;AACH,SAAgB,YAAY,CAAC,IAU5B;IACG,IAAI,IAAI,CAAC,mBAAmB;QAAE,OAAO,qBAAa,CAAC,GAAG,CAAC;IAEvD,MAAM,WAAW,GAAG,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IAC3D,IAAI,+BAAuB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAAE,OAAO,qBAAa,CAAC,MAAM,CAAC;IAC1G,MAAM,UAAU,GAAG,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACzD,IAAI,2BAAmB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAAE,OAAO,qBAAa,CAAC,MAAM,CAAC;IACrG,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACnD,MAAM,MAAM,GAAG,8BAAsB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IACrE,OAAO,MAAM,CAAC,CAAC,CAAC,qBAAa,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC7D,CAAC;AAED,kEAAkE;AACrD,QAAA,oBAAoB,GAAG,qDAAqD,CAAC;AAE1F;;;;;GAKG;AACH,SAAgB,gBAAgB,CAAC,IAAY,EAAE,cAAsB;IACjE,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,MAAM,MAAM,GAAG,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACpD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,4BAAoB,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC;QACvE,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC3D,IAAI,OAAO,KAAK,MAAM;YAAE,SAAS;QACjC,IAAI,6BAAqB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAAE,SAAS;QACjF,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,SAAS;QACtC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpB,IAAI,KAAK,CAAC,MAAM,IAAI,6BAAqB;YAAE,MAAM;IACrD,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC","sourcesContent":["// What counts as a bounce, defined once for both platforms.\n//\n// Windows classifies inside the generated PowerShell — an item's body is only\n// read once a cheaper signal has matched, which keeps a large-inbox scan\n// affordable — while macOS classifies here after a bulk property read. Two\n// implementations of the RULES is how \"what is a bounce\" comes to mean two\n// things, so every list and limit lives here and the PowerShell is generated\n// from them (see windows/bounces.ts).\n//\n// Deliberately specific, mail-system wording: ordinary mail that merely\n// mentions a \"delivery\" must never match, because the next step is often to\n// delete what did.\n\n/** Subject fragments only a mail system writes. Matched case-insensitively. */\nexport const BOUNCE_SUBJECT_PHRASES: readonly string[] = [\n 'undeliverable',\n 'message blocked',\n 'mail delivery failed',\n 'delivery status notification (failure)',\n 'delivery has failed',\n 'failure notice',\n 'undelivered mail returned to sender',\n 'returned mail',\n];\n\n/** Fragments of a sender ADDRESS that fingerprint a mail-delivery daemon. */\nexport const BOUNCE_DAEMON_ADDRESSES: readonly string[] = [\n 'mailer-daemon',\n 'mailer_daemon',\n 'postmaster@',\n 'mail-daemon',\n 'maildelivery',\n];\n\n/** Fragments of a sender DISPLAY NAME that fingerprint a mail-delivery daemon. */\nexport const BOUNCE_DAEMON_NAMES: readonly string[] = [\n 'mail delivery subsystem',\n 'mail delivery system',\n 'microsoft outlook',\n 'postmaster',\n 'mailer-daemon',\n 'mail administrator',\n 'internet mail delivery',\n];\n\n/** MessageClass prefix Exchange stamps on a non-delivery report. */\nexport const NDR_MESSAGE_CLASS_PREFIX = 'REPORT.IPM.Note.NDR';\n\n/**\n * The same structural signal as it arrives on the wire, for macOS: a bounce is\n * a delivery-status notification (RFC 3464) and says so in its own headers.\n * Both markers must be present — `multipart/report` also carries read receipts,\n * and it is the report type that separates a delivery that FAILED from one that\n * was acknowledged.\n *\n * Outlook for Mac publishes no MessageClass in its AppleScript dictionary, so\n * this stands in for it: the message Exchange stamps REPORT.IPM.Note.NDR is the\n * message that arrived carrying these headers.\n *\n * The macOS index tests these in AppleScript, whose `contains` is already\n * case-insensitive, and returns one flag per message rather than the headers\n * themselves (see mac/bounces.ts).\n */\nexport const DSN_HEADER_MARKERS: readonly string[] = ['multipart/report', 'delivery-status'];\n\n/** Address fragments never reported as a failed recipient — they are the bounce's own author. */\nexport const DAEMON_SELF_ADDRESSES: readonly string[] = ['mailer-daemon', 'postmaster', 'mail-daemon'];\n\n/** At most this many failed recipients per bounce; a list bounce can name hundreds. */\nexport const MAX_FAILED_RECIPIENTS = 5;\n\n/** The reasons a bounce is flagged, worded once. */\nexport const BOUNCE_REASON = {\n ndr: 'Non-delivery report (NDR)',\n daemon: 'From mail-delivery system',\n subjectPhrase: (phrase: string) => `Bounce subject phrase: '${phrase}'`,\n} as const;\n\n/**\n * Why this item is a bounce, or '' if it isn't.\n *\n * The structural signal (a non-delivery report) outranks the sender\n * fingerprint, which outranks a subject phrase — so the reason reported is the\n * strongest one that applied, not whichever was tested first.\n */\nexport function bounceReason(item: {\n subject?: string;\n senderName?: string;\n senderEmail?: string;\n /**\n * True when the item is STRUCTURALLY a non-delivery report, whichever way\n * its platform can see that: its MessageClass on Windows, the\n * delivery-status headers it arrived with on macOS.\n */\n isNonDeliveryReport?: boolean;\n}): string {\n if (item.isNonDeliveryReport) return BOUNCE_REASON.ndr;\n\n const senderEmail = (item.senderEmail || '').toLowerCase();\n if (BOUNCE_DAEMON_ADDRESSES.some(fragment => senderEmail.includes(fragment))) return BOUNCE_REASON.daemon;\n const senderName = (item.senderName || '').toLowerCase();\n if (BOUNCE_DAEMON_NAMES.some(fragment => senderName.includes(fragment))) return BOUNCE_REASON.daemon;\n const subject = (item.subject || '').toLowerCase();\n const phrase = BOUNCE_SUBJECT_PHRASES.find(p => subject.includes(p));\n return phrase ? BOUNCE_REASON.subjectPhrase(phrase) : '';\n}\n\n/** The address pattern both platforms scan a bounce body with. */\nexport const BODY_ADDRESS_PATTERN = '[A-Za-z0-9._%+\\\\-]+@[A-Za-z0-9.\\\\-]+\\\\.[A-Za-z]{2,}';\n\n/**\n * The addresses a bounce body names as having failed — best effort, since a\n * bounce body is prose with no agreed structure. The account's own address and\n * the daemon's are dropped: both appear in nearly every bounce and neither is a\n * send that failed.\n */\nexport function failedRecipients(body: string, accountAddress: string): string[] {\n if (!body) return [];\n const target = (accountAddress || '').toLowerCase();\n const found: string[] = [];\n for (const match of body.matchAll(new RegExp(BODY_ADDRESS_PATTERN, 'g'))) {\n const address = match[0].toLowerCase().replace(/\\.+$/, '');\n if (address === target) continue;\n if (DAEMON_SELF_ADDRESSES.some(fragment => address.includes(fragment))) continue;\n if (found.includes(address)) continue;\n found.push(address);\n if (found.length >= MAX_FAILED_RECIPIENTS) break;\n }\n return found;\n}\n"]}
@@ -0,0 +1,21 @@
1
+ import { type ScriptRunner } from '../errors';
2
+ import { type RunBudget } from '../runtime';
3
+ export interface ScriptProcess {
4
+ runner: ScriptRunner;
5
+ command: string;
6
+ args: readonly string[];
7
+ /** The script exactly as it runs, for errors and the debug hook. */
8
+ script: string;
9
+ budget: RunBudget;
10
+ /** The script's own failure message, and the line it came from, out of stderr. */
11
+ parseFailure(stderr: string): {
12
+ message: string;
13
+ line?: number;
14
+ };
15
+ }
16
+ export interface ProcessOutput {
17
+ stdout: string;
18
+ durationMs: number;
19
+ }
20
+ /** Run one script to completion, rejecting with the most specific OutlookError on failure. */
21
+ export declare function runScriptProcess(run: ScriptProcess): Promise<ProcessOutput>;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runScriptProcess = runScriptProcess;
4
+ // Running one interpreter process: the part both platforms' runners share.
5
+ //
6
+ // Each platform decides how its script reaches the interpreter and how a
7
+ // failure reads in that interpreter's stderr. Everything else — the settings a
8
+ // run takes, telling an abort from a timeout from an overflow, and reporting
9
+ // the run to the debug hook — happens here, once.
10
+ const child_process_1 = require("child_process");
11
+ const errors_1 = require("../errors");
12
+ const runtime_1 = require("../runtime");
13
+ /** Run one script to completion, rejecting with the most specific OutlookError on failure. */
14
+ function runScriptProcess(run) {
15
+ const { maxBufferBytes, signal } = (0, runtime_1.getConfig)();
16
+ const timeout = (0, runtime_1.timeoutFor)(run.budget);
17
+ const startedAt = Date.now();
18
+ return new Promise((resolve, reject) => {
19
+ (0, child_process_1.execFile)(run.command, [...run.args],
20
+ // windowsHide keeps a GUI host (an Electron app, say) from flashing a
21
+ // console window on every run.
22
+ { maxBuffer: maxBufferBytes, timeout, signal, windowsHide: true, encoding: 'utf8' }, (error, stdout, stderr) => {
23
+ const durationMs = Date.now() - startedAt;
24
+ if (error) {
25
+ const { message, line } = run.parseFailure(stderr ?? '');
26
+ const failure = (0, errors_1.classifyRunFailure)({
27
+ runner: run.runner,
28
+ script: run.script,
29
+ stderr: stderr ?? '',
30
+ message,
31
+ line,
32
+ durationMs,
33
+ nodeError: error,
34
+ timeoutMs: timeout,
35
+ maxBufferBytes,
36
+ signal,
37
+ });
38
+ (0, runtime_1.reportRun)({ runner: run.runner, script: run.script, durationMs, error: failure.message });
39
+ reject(failure);
40
+ return;
41
+ }
42
+ (0, runtime_1.reportRun)({ runner: run.runner, script: run.script, durationMs });
43
+ resolve({ stdout, durationMs });
44
+ });
45
+ });
46
+ }
47
+ //# sourceMappingURL=exec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/shared/exec.ts"],"names":[],"mappings":";;AA4BA,4CAoCC;AAhED,2EAA2E;AAC3E,EAAE;AACF,yEAAyE;AACzE,+EAA+E;AAC/E,6EAA6E;AAC7E,kDAAkD;AAClD,iDAAyC;AACzC,sCAAkE;AAClE,wCAA8E;AAmB9E,8FAA8F;AAC9F,SAAgB,gBAAgB,CAAC,GAAkB;IAC/C,MAAM,EAAC,cAAc,EAAE,MAAM,EAAC,GAAG,IAAA,mBAAS,GAAE,CAAC;IAC7C,MAAM,OAAO,GAAG,IAAA,oBAAU,EAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACvC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACnC,IAAA,wBAAQ,EACJ,GAAG,CAAC,OAAO,EACX,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC;QACb,sEAAsE;QACtE,+BAA+B;QAC/B,EAAC,SAAS,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAC,EACjF,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACtB,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;YAC1C,IAAI,KAAK,EAAE,CAAC;gBACR,MAAM,EAAC,OAAO,EAAE,IAAI,EAAC,GAAG,GAAG,CAAC,YAAY,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;gBACvD,MAAM,OAAO,GAAG,IAAA,2BAAkB,EAAC;oBAC/B,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,MAAM,EAAE,MAAM,IAAI,EAAE;oBACpB,OAAO;oBACP,IAAI;oBACJ,UAAU;oBACV,SAAS,EAAE,KAAK;oBAChB,SAAS,EAAE,OAAO;oBAClB,cAAc;oBACd,MAAM;iBACT,CAAC,CAAC;gBACH,IAAA,mBAAS,EAAC,EAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,EAAC,CAAC,CAAC;gBACxF,MAAM,CAAC,OAAO,CAAC,CAAC;gBAChB,OAAO;YACX,CAAC;YACD,IAAA,mBAAS,EAAC,EAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,EAAC,CAAC,CAAC;YAChE,OAAO,CAAC,EAAC,MAAM,EAAE,UAAU,EAAC,CAAC,CAAC;QAClC,CAAC,CACJ,CAAC;IACN,CAAC,CAAC,CAAC;AACP,CAAC","sourcesContent":["// Running one interpreter process: the part both platforms' runners share.\n//\n// Each platform decides how its script reaches the interpreter and how a\n// failure reads in that interpreter's stderr. Everything else — the settings a\n// run takes, telling an abort from a timeout from an overflow, and reporting\n// the run to the debug hook — happens here, once.\nimport { execFile } from 'child_process';\nimport { classifyRunFailure, type ScriptRunner } from '../errors';\nimport { getConfig, reportRun, type RunBudget, timeoutFor } from '../runtime';\n\nexport interface ScriptProcess {\n runner: ScriptRunner;\n command: string;\n args: readonly string[];\n /** The script exactly as it runs, for errors and the debug hook. */\n script: string;\n budget: RunBudget;\n\n /** The script's own failure message, and the line it came from, out of stderr. */\n parseFailure(stderr: string): { message: string; line?: number };\n}\n\nexport interface ProcessOutput {\n stdout: string;\n durationMs: number;\n}\n\n/** Run one script to completion, rejecting with the most specific OutlookError on failure. */\nexport function runScriptProcess(run: ScriptProcess): Promise<ProcessOutput> {\n const {maxBufferBytes, signal} = getConfig();\n const timeout = timeoutFor(run.budget);\n const startedAt = Date.now();\n return new Promise((resolve, reject) => {\n execFile(\n run.command,\n [...run.args],\n // windowsHide keeps a GUI host (an Electron app, say) from flashing a\n // console window on every run.\n {maxBuffer: maxBufferBytes, timeout, signal, windowsHide: true, encoding: 'utf8'},\n (error, stdout, stderr) => {\n const durationMs = Date.now() - startedAt;\n if (error) {\n const {message, line} = run.parseFailure(stderr ?? '');\n const failure = classifyRunFailure({\n runner: run.runner,\n script: run.script,\n stderr: stderr ?? '',\n message,\n line,\n durationMs,\n nodeError: error,\n timeoutMs: timeout,\n maxBufferBytes,\n signal,\n });\n reportRun({runner: run.runner, script: run.script, durationMs, error: failure.message});\n reject(failure);\n return;\n }\n reportRun({runner: run.runner, script: run.script, durationMs});\n resolve({stdout, durationMs});\n },\n );\n });\n}\n"]}
@@ -1,11 +1,17 @@
1
- /** Parse a script's stdout as an object, tolerating empty output and 'null'. */
2
- export declare function parseObject(raw: string): Record<string, unknown>;
3
- /** Parse a script's stdout as a list, absorbing ConvertTo-Json's unwrapping. */
4
- export declare function parseArray(raw: string): unknown[];
5
- /** One value as a list: an array stays, null becomes empty, anything else wraps. */
1
+ /** One value as a list: an array stays, null and undefined become empty, anything else wraps. */
6
2
  export declare function toArray(value: unknown): unknown[];
7
- /** A record's field as a plain object, for mapping a list of rows. */
3
+ /** A value as a plain object, for reading fields off one row. */
8
4
  export declare function record(value: unknown): Record<string, unknown>;
9
5
  export declare function str(value: unknown): string;
10
6
  export declare function num(value: unknown): number;
7
+ export declare function bool(value: unknown): boolean;
8
+ /** A list of non-empty strings. */
11
9
  export declare function strList(value: unknown): string[];
10
+ /** A base64 field decoded as UTF-8 — how scripts pass bodies that must survive intact. */
11
+ export declare function base64Text(value: unknown): string;
12
+ /** A list of per-item failures, as the scripts report them. */
13
+ export declare function itemFailures(value: unknown): {
14
+ entryId: string;
15
+ subject: string;
16
+ error: string;
17
+ }[];
@@ -1,45 +1,51 @@
1
1
  "use strict";
2
- // Coercion helpers for the JSON the PowerShell scripts emit.
2
+ // Coercing the JSON a PowerShell script prints into the package's types.
3
3
  //
4
- // PowerShell's ConvertTo-Json has two habits that a consumer must never see:
5
- // it unwraps a single-element array to a bare object, and renders an empty one
6
- // as null. Every list crossing that boundary therefore goes through `toArray`,
7
- // and every scalar through a coercion that survives a missing property.
4
+ // ConvertTo-Json has two habits a caller must never see: it unwraps a
5
+ // single-element array into a bare object, and it renders an empty one as
6
+ // nothing at all. Every list crossing that boundary goes through `toArray`, and
7
+ // every scalar through a coercion that survives a missing property.
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.parseObject = parseObject;
10
- exports.parseArray = parseArray;
11
9
  exports.toArray = toArray;
12
10
  exports.record = record;
13
11
  exports.str = str;
14
12
  exports.num = num;
13
+ exports.bool = bool;
15
14
  exports.strList = strList;
16
- /** Parse a script's stdout as an object, tolerating empty output and 'null'. */
17
- function parseObject(raw) {
18
- if (!raw || !raw.trim() || raw.trim() === 'null')
19
- return {};
20
- return JSON.parse(raw.trim());
21
- }
22
- /** Parse a script's stdout as a list, absorbing ConvertTo-Json's unwrapping. */
23
- function parseArray(raw) {
24
- if (!raw || !raw.trim() || raw.trim() === 'null')
25
- return [];
26
- return toArray(JSON.parse(raw.trim()));
27
- }
28
- /** One value as a list: an array stays, null becomes empty, anything else wraps. */
15
+ exports.base64Text = base64Text;
16
+ exports.itemFailures = itemFailures;
17
+ /** One value as a list: an array stays, null and undefined become empty, anything else wraps. */
29
18
  function toArray(value) {
30
19
  return Array.isArray(value) ? value : value == null ? [] : [value];
31
20
  }
32
- /** A record's field as a plain object, for mapping a list of rows. */
21
+ /** A value as a plain object, for reading fields off one row. */
33
22
  function record(value) {
34
- return (value ?? {});
23
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
24
+ ? value
25
+ : {};
35
26
  }
36
27
  function str(value) {
37
28
  return value == null ? '' : String(value);
38
29
  }
39
30
  function num(value) {
40
- return typeof value === 'number' ? value : 0;
31
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
41
32
  }
33
+ function bool(value) {
34
+ return value === true;
35
+ }
36
+ /** A list of non-empty strings. */
42
37
  function strList(value) {
43
38
  return toArray(value).map(str).filter(Boolean);
44
39
  }
40
+ /** A base64 field decoded as UTF-8 — how scripts pass bodies that must survive intact. */
41
+ function base64Text(value) {
42
+ return Buffer.from(str(value), 'base64').toString('utf8');
43
+ }
44
+ /** A list of per-item failures, as the scripts report them. */
45
+ function itemFailures(value) {
46
+ return toArray(value).map(row => {
47
+ const e = record(row);
48
+ return { entryId: str(e.entryId), subject: str(e.subject), error: str(e.error) };
49
+ });
50
+ }
45
51
  //# sourceMappingURL=json.js.map