@dawnlit/outlook-bridge 1.1.0 → 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.
- package/README.md +150 -183
- package/dist/backend.d.ts +151 -0
- package/dist/backend.js +3 -0
- package/dist/backend.js.map +1 -0
- package/dist/bridge.d.ts +56 -0
- package/dist/bridge.js +507 -0
- package/dist/bridge.js.map +1 -0
- package/dist/errors.d.ts +81 -43
- package/dist/errors.js +98 -52
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +10 -10
- package/dist/index.js +25 -52
- package/dist/index.js.map +1 -1
- package/dist/mac/accounts.d.ts +7 -2
- package/dist/mac/accounts.js +23 -6
- package/dist/mac/accounts.js.map +1 -1
- package/dist/mac/attachments.d.ts +5 -25
- package/dist/mac/attachments.js +40 -99
- package/dist/mac/attachments.js.map +1 -1
- package/dist/mac/bounces.d.ts +12 -27
- package/dist/mac/bounces.js +95 -112
- package/dist/mac/bounces.js.map +1 -1
- package/dist/mac/cleanup.d.ts +11 -25
- package/dist/mac/cleanup.js +50 -76
- package/dist/mac/cleanup.js.map +1 -1
- package/dist/mac/drafts.d.ts +13 -34
- package/dist/mac/drafts.js +95 -169
- package/dist/mac/drafts.js.map +1 -1
- package/dist/mac/folders.d.ts +10 -21
- package/dist/mac/folders.js +33 -47
- package/dist/mac/folders.js.map +1 -1
- package/dist/mac/index.d.ts +2 -22
- package/dist/mac/index.js +19 -81
- package/dist/mac/index.js.map +1 -1
- package/dist/mac/profile.d.ts +13 -0
- package/dist/mac/profile.js +149 -0
- package/dist/mac/profile.js.map +1 -0
- package/dist/mac/read.d.ts +18 -36
- package/dist/mac/read.js +154 -208
- package/dist/mac/read.js.map +1 -1
- package/dist/mac/run.d.ts +35 -39
- package/dist/mac/run.js +97 -102
- package/dist/mac/run.js.map +1 -1
- package/dist/mac/scripts.d.ts +78 -86
- package/dist/mac/scripts.js +125 -115
- package/dist/mac/scripts.js.map +1 -1
- package/dist/mac/send.d.ts +10 -20
- package/dist/mac/send.js +64 -86
- package/dist/mac/send.js.map +1 -1
- package/dist/mac/signatures.d.ts +2 -2
- package/dist/mac/signatures.js +9 -11
- package/dist/mac/signatures.js.map +1 -1
- package/dist/mac/templates.d.ts +14 -26
- package/dist/mac/templates.js +75 -113
- package/dist/mac/templates.js.map +1 -1
- package/dist/mail.d.ts +77 -45
- package/dist/mail.js +166 -73
- package/dist/mail.js.map +1 -1
- package/dist/runtime.d.ts +76 -43
- package/dist/runtime.js +103 -48
- package/dist/runtime.js.map +1 -1
- package/dist/shared/args.d.ts +48 -0
- package/dist/shared/args.js +216 -0
- package/dist/shared/args.js.map +1 -0
- package/dist/shared/attachmentMatch.d.ts +28 -16
- package/dist/shared/attachmentMatch.js +60 -22
- package/dist/shared/attachmentMatch.js.map +1 -1
- package/dist/shared/bounceReport.d.ts +11 -0
- package/dist/shared/bounceReport.js +45 -0
- package/dist/shared/bounceReport.js.map +1 -0
- package/dist/shared/bounceRules.d.ts +38 -10
- package/dist/shared/bounceRules.js +50 -32
- package/dist/shared/bounceRules.js.map +1 -1
- package/dist/shared/exec.d.ts +21 -0
- package/dist/shared/exec.js +47 -0
- package/dist/shared/exec.js.map +1 -0
- package/dist/shared/json.d.ts +12 -6
- package/dist/shared/json.js +29 -23
- package/dist/shared/json.js.map +1 -1
- package/dist/shared/locate.d.ts +39 -0
- package/dist/shared/locate.js +129 -0
- package/dist/shared/locate.js.map +1 -0
- package/dist/shared/replyBody.d.ts +42 -32
- package/dist/shared/replyBody.js +84 -71
- package/dist/shared/replyBody.js.map +1 -1
- package/dist/templateBody.d.ts +63 -0
- package/dist/{outlookTemplateSections.js → templateBody.js} +72 -72
- package/dist/templateBody.js.map +1 -0
- package/dist/types.d.ts +360 -165
- package/dist/types.js +8 -9
- package/dist/types.js.map +1 -1
- package/dist/windows/accounts.d.ts +10 -1
- package/dist/windows/accounts.js +25 -13
- package/dist/windows/accounts.js.map +1 -1
- package/dist/windows/attachments.d.ts +9 -24
- package/dist/windows/attachments.js +94 -119
- package/dist/windows/attachments.js.map +1 -1
- package/dist/windows/bounces.d.ts +13 -26
- package/dist/windows/bounces.js +169 -200
- package/dist/windows/bounces.js.map +1 -1
- package/dist/windows/cleanup.d.ts +19 -23
- package/dist/windows/cleanup.js +93 -111
- package/dist/windows/cleanup.js.map +1 -1
- package/dist/windows/drafts.d.ts +16 -35
- package/dist/windows/drafts.js +167 -253
- package/dist/windows/drafts.js.map +1 -1
- package/dist/windows/folders.d.ts +8 -13
- package/dist/windows/folders.js +45 -52
- package/dist/windows/folders.js.map +1 -1
- package/dist/windows/index.d.ts +2 -27
- package/dist/windows/index.js +11 -65
- package/dist/windows/index.js.map +1 -1
- package/dist/windows/read.d.ts +24 -45
- package/dist/windows/read.js +266 -279
- package/dist/windows/read.js.map +1 -1
- package/dist/windows/run.d.ts +39 -39
- package/dist/windows/run.js +151 -111
- package/dist/windows/run.js.map +1 -1
- package/dist/windows/scripts.d.ts +71 -38
- package/dist/windows/scripts.js +123 -71
- package/dist/windows/scripts.js.map +1 -1
- package/dist/windows/send.d.ts +6 -11
- package/dist/windows/send.js +70 -79
- package/dist/windows/send.js.map +1 -1
- package/dist/windows/signatures.d.ts +4 -10
- package/dist/windows/signatures.js +30 -37
- package/dist/windows/signatures.js.map +1 -1
- package/dist/windows/templates.d.ts +12 -18
- package/dist/windows/templates.js +126 -151
- package/dist/windows/templates.js.map +1 -1
- package/package.json +5 -4
- package/dist/OutlookService.d.ts +0 -43
- package/dist/OutlookService.js +0 -137
- package/dist/OutlookService.js.map +0 -1
- package/dist/outlookTemplateSections.d.ts +0 -62
- package/dist/outlookTemplateSections.js.map +0 -1
- package/dist/shared/capabilities.d.ts +0 -3
- package/dist/shared/capabilities.js +0 -9
- package/dist/shared/capabilities.js.map +0 -1
package/dist/shared/json.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json.js","sourceRoot":"","sources":["../../src/shared/json.ts"],"names":[],"mappings":";AAAA,
|
|
1
|
+
{"version":3,"file":"json.js","sourceRoot":"","sources":["../../src/shared/json.ts"],"names":[],"mappings":";AAAA,yEAAyE;AACzE,EAAE;AACF,sEAAsE;AACtE,0EAA0E;AAC1E,gFAAgF;AAChF,oEAAoE;;AAGpE,0BAEC;AAGD,wBAIC;AAED,kBAEC;AAED,kBAEC;AAED,oBAEC;AAGD,0BAEC;AAGD,gCAEC;AAGD,oCAKC;AAxCD,iGAAiG;AACjG,SAAgB,OAAO,CAAC,KAAc;IAClC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AACvE,CAAC;AAED,iEAAiE;AACjE,SAAgB,MAAM,CAAC,KAAc;IACjC,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACvE,CAAC,CAAC,KAAgC;QAClC,CAAC,CAAC,EAAE,CAAC;AACb,CAAC;AAED,SAAgB,GAAG,CAAC,KAAc;IAC9B,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC9C,CAAC;AAED,SAAgB,GAAG,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC3E,CAAC;AAED,SAAgB,IAAI,CAAC,KAAc;IAC/B,OAAO,KAAK,KAAK,IAAI,CAAC;AAC1B,CAAC;AAED,mCAAmC;AACnC,SAAgB,OAAO,CAAC,KAAc;IAClC,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AACnD,CAAC;AAED,0FAA0F;AAC1F,SAAgB,UAAU,CAAC,KAAc;IACrC,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC9D,CAAC;AAED,+DAA+D;AAC/D,SAAgB,YAAY,CAAC,KAAc;IACvC,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;QAC5B,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QACtB,OAAO,EAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,EAAC,CAAC;IACnF,CAAC,CAAC,CAAC;AACP,CAAC","sourcesContent":["// Coercing the JSON a PowerShell script prints into the package's types.\n//\n// ConvertTo-Json has two habits a caller must never see: it unwraps a\n// single-element array into a bare object, and it renders an empty one as\n// nothing at all. Every list crossing that boundary goes through `toArray`, and\n// every scalar through a coercion that survives a missing property.\n\n/** One value as a list: an array stays, null and undefined become empty, anything else wraps. */\nexport function toArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : value == null ? [] : [value];\n}\n\n/** A value as a plain object, for reading fields off one row. */\nexport function record(value: unknown): Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n ? value as Record<string, unknown>\n : {};\n}\n\nexport function str(value: unknown): string {\n return value == null ? '' : String(value);\n}\n\nexport function num(value: unknown): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0;\n}\n\nexport function bool(value: unknown): boolean {\n return value === true;\n}\n\n/** A list of non-empty strings. */\nexport function strList(value: unknown): string[] {\n return toArray(value).map(str).filter(Boolean);\n}\n\n/** A base64 field decoded as UTF-8 — how scripts pass bodies that must survive intact. */\nexport function base64Text(value: unknown): string {\n return Buffer.from(str(value), 'base64').toString('utf8');\n}\n\n/** A list of per-item failures, as the scripts report them. */\nexport function itemFailures(value: unknown): { entryId: string; subject: string; error: string }[] {\n return toArray(value).map(row => {\n const e = record(row);\n return {entryId: str(e.entryId), subject: str(e.subject), error: str(e.error)};\n });\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { InboxSearchMatch } from '../types';
|
|
2
|
+
/** The fields a match is judged on, trimmed; blank means not given. */
|
|
3
|
+
export interface WantedEmail {
|
|
4
|
+
readonly subject?: string;
|
|
5
|
+
readonly sender?: string;
|
|
6
|
+
readonly receivedTime?: string;
|
|
7
|
+
}
|
|
8
|
+
/** INVALID_REQUEST unless the description gives enough to tell one email from its thread. */
|
|
9
|
+
export declare function requireDescribed(wanted: WantedEmail): void;
|
|
10
|
+
/**
|
|
11
|
+
* The subject glob to prefilter a search with: the longest run of plain
|
|
12
|
+
* characters in the subject, or undefined when there's no run of four.
|
|
13
|
+
*
|
|
14
|
+
* A subject can hold an apostrophe or a `*`, and the prefilter becomes a query
|
|
15
|
+
* on some stores; it only has to be selective, since every result is judged on
|
|
16
|
+
* the whole subject afterwards.
|
|
17
|
+
*/
|
|
18
|
+
export declare function subjectPrefilter(subject: string | undefined): string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* How far back a search has to reach to cover the day the email arrived, or
|
|
21
|
+
* `fallback` when no day is known. Two days of slack cover whichever timezone
|
|
22
|
+
* the stamp was written in, and a search started either side of midnight.
|
|
23
|
+
*/
|
|
24
|
+
export declare function daysBackFor(receivedTime: string | undefined, fallback: number, now?: number): number;
|
|
25
|
+
/**
|
|
26
|
+
* How well a found email answers the description, or 0 for "not this one".
|
|
27
|
+
*
|
|
28
|
+
* Disagreement on any given field disqualifies outright. Agreement is then
|
|
29
|
+
* ranked, so an exact minute beats the same day: a sender who wrote twice in
|
|
30
|
+
* one afternoon still resolves to the email described.
|
|
31
|
+
*/
|
|
32
|
+
export declare function matchScore(wanted: WantedEmail, match: InboxSearchMatch): number;
|
|
33
|
+
/** The best match, or null when nothing agrees with the description. */
|
|
34
|
+
export declare function bestMatch(wanted: WantedEmail, matches: readonly InboxSearchMatch[]): InboxSearchMatch | null;
|
|
35
|
+
/**
|
|
36
|
+
* Accounts in the order to search them: the one a listing's folderPath names
|
|
37
|
+
* first (`\\team@example.com\Inbox\…`), then the rest as given.
|
|
38
|
+
*/
|
|
39
|
+
export declare function accountSearchOrder(accounts: readonly string[], folderPath: string | undefined): string[];
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requireDescribed = requireDescribed;
|
|
4
|
+
exports.subjectPrefilter = subjectPrefilter;
|
|
5
|
+
exports.daysBackFor = daysBackFor;
|
|
6
|
+
exports.matchScore = matchScore;
|
|
7
|
+
exports.bestMatch = bestMatch;
|
|
8
|
+
exports.accountSearchOrder = accountSearchOrder;
|
|
9
|
+
// Finding an email again from what was kept about it: the rules, as pure
|
|
10
|
+
// functions. The search itself is an ordinary searchInboxByFilter per mailbox;
|
|
11
|
+
// what lives here is how narrow that search can be made, and which of its
|
|
12
|
+
// results is the email.
|
|
13
|
+
//
|
|
14
|
+
// The rules never guess. Every field the description gives has to agree, and at
|
|
15
|
+
// least two have to be given, because a subject alone names every message in a
|
|
16
|
+
// thread. A miss stays a miss rather than returning somebody else's email.
|
|
17
|
+
const mail_1 = require("../mail");
|
|
18
|
+
const errors_1 = require("../errors");
|
|
19
|
+
/** `yyyy-MM-dd`, optionally followed by ` HH:mm` — how both platforms stamp a received time. */
|
|
20
|
+
const STAMP = /^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2})?/;
|
|
21
|
+
/** An address inside a sender string, which may be a name, an address, or both. */
|
|
22
|
+
const ADDRESS = /[^\s<>,;"']+@[^\s<>,;"']+/;
|
|
23
|
+
const DAY_MS = 86_400_000;
|
|
24
|
+
const subjectKey = (subject) => (0, mail_1.threadSubject)(subject).toLowerCase();
|
|
25
|
+
/**
|
|
26
|
+
* The stamp in the listings' own form (`yyyy-MM-dd HH:mm`), or '' when it isn't
|
|
27
|
+
* a stamp at all — which is then treated as not given, since it can't disagree
|
|
28
|
+
* with anything.
|
|
29
|
+
*/
|
|
30
|
+
function stampOf(receivedTime) {
|
|
31
|
+
const match = STAMP.exec((receivedTime ?? '').trim());
|
|
32
|
+
return match ? match[0].replace('T', ' ') : '';
|
|
33
|
+
}
|
|
34
|
+
/** How many of the three fields the description actually gives. */
|
|
35
|
+
function recordedCount(wanted) {
|
|
36
|
+
return (wanted.subject ? 1 : 0) + (wanted.sender ? 1 : 0) + (stampOf(wanted.receivedTime) ? 1 : 0);
|
|
37
|
+
}
|
|
38
|
+
/** INVALID_REQUEST unless the description gives enough to tell one email from its thread. */
|
|
39
|
+
function requireDescribed(wanted) {
|
|
40
|
+
if (recordedCount(wanted) < 2) {
|
|
41
|
+
throw new errors_1.InvalidRequestError('Describe the email by at least two of subject, sender and receivedTime (as yyyy-MM-dd HH:mm): '
|
|
42
|
+
+ 'a subject alone names every message in its thread.');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The subject glob to prefilter a search with: the longest run of plain
|
|
47
|
+
* characters in the subject, or undefined when there's no run of four.
|
|
48
|
+
*
|
|
49
|
+
* A subject can hold an apostrophe or a `*`, and the prefilter becomes a query
|
|
50
|
+
* on some stores; it only has to be selective, since every result is judged on
|
|
51
|
+
* the whole subject afterwards.
|
|
52
|
+
*/
|
|
53
|
+
function subjectPrefilter(subject) {
|
|
54
|
+
const longest = ((0, mail_1.threadSubject)(subject ?? '').match(/[A-Za-z0-9 ._-]+/g) ?? [])
|
|
55
|
+
.map(run => run.trim())
|
|
56
|
+
.sort((a, b) => b.length - a.length)[0] ?? '';
|
|
57
|
+
return longest.length >= 4 ? `*${longest}*` : undefined;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* How far back a search has to reach to cover the day the email arrived, or
|
|
61
|
+
* `fallback` when no day is known. Two days of slack cover whichever timezone
|
|
62
|
+
* the stamp was written in, and a search started either side of midnight.
|
|
63
|
+
*/
|
|
64
|
+
function daysBackFor(receivedTime, fallback, now = Date.now()) {
|
|
65
|
+
const stamp = stampOf(receivedTime);
|
|
66
|
+
if (!stamp)
|
|
67
|
+
return fallback;
|
|
68
|
+
const day = Date.parse(`${stamp.slice(0, 10)}T00:00:00Z`);
|
|
69
|
+
if (Number.isNaN(day))
|
|
70
|
+
return fallback;
|
|
71
|
+
return Math.max(1, Math.floor((now - day) / DAY_MS) + 2);
|
|
72
|
+
}
|
|
73
|
+
/** Whether a found email came from the sender described. */
|
|
74
|
+
function senderMatches(wanted, match) {
|
|
75
|
+
const want = wanted.toLowerCase();
|
|
76
|
+
const address = ADDRESS.exec(want)?.[0];
|
|
77
|
+
if (address)
|
|
78
|
+
return address === match.senderEmail.trim().toLowerCase();
|
|
79
|
+
const name = match.senderName.trim().toLowerCase();
|
|
80
|
+
return name !== '' && (want.includes(name) || name.includes(want));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* How well a found email answers the description, or 0 for "not this one".
|
|
84
|
+
*
|
|
85
|
+
* Disagreement on any given field disqualifies outright. Agreement is then
|
|
86
|
+
* ranked, so an exact minute beats the same day: a sender who wrote twice in
|
|
87
|
+
* one afternoon still resolves to the email described.
|
|
88
|
+
*/
|
|
89
|
+
function matchScore(wanted, match) {
|
|
90
|
+
if (recordedCount(wanted) < 2)
|
|
91
|
+
return 0;
|
|
92
|
+
const wantSubject = wanted.subject ? subjectKey(wanted.subject) : '';
|
|
93
|
+
if (wantSubject && subjectKey(match.subject) !== wantSubject)
|
|
94
|
+
return 0;
|
|
95
|
+
if (wanted.sender && !senderMatches(wanted.sender, match))
|
|
96
|
+
return 0;
|
|
97
|
+
const stamp = stampOf(wanted.receivedTime);
|
|
98
|
+
const found = match.receivedTime.trim();
|
|
99
|
+
const sameDay = stamp !== '' && stamp.slice(0, 10) === found.slice(0, 10);
|
|
100
|
+
const sameMinute = sameDay && stamp.length >= 16 && stamp.slice(0, 16) === found.slice(0, 16);
|
|
101
|
+
if (stamp && !sameDay)
|
|
102
|
+
return 0;
|
|
103
|
+
return (wantSubject ? 1 : 0) + (wanted.sender ? 2 : 0) + (sameMinute ? 3 : sameDay ? 1 : 0);
|
|
104
|
+
}
|
|
105
|
+
/** The best match, or null when nothing agrees with the description. */
|
|
106
|
+
function bestMatch(wanted, matches) {
|
|
107
|
+
let best = null;
|
|
108
|
+
let bestScore = 0;
|
|
109
|
+
for (const match of matches) {
|
|
110
|
+
const score = matchScore(wanted, match);
|
|
111
|
+
if (score > bestScore) {
|
|
112
|
+
best = match;
|
|
113
|
+
bestScore = score;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return best;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Accounts in the order to search them: the one a listing's folderPath names
|
|
120
|
+
* first (`\\team@example.com\Inbox\…`), then the rest as given.
|
|
121
|
+
*/
|
|
122
|
+
function accountSearchOrder(accounts, folderPath) {
|
|
123
|
+
const path = (folderPath ?? '').toLowerCase();
|
|
124
|
+
if (!path)
|
|
125
|
+
return [...accounts];
|
|
126
|
+
const named = accounts.filter(account => path.includes(account.toLowerCase()));
|
|
127
|
+
return [...named, ...accounts.filter(account => !named.includes(account))];
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=locate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locate.js","sourceRoot":"","sources":["../../src/shared/locate.ts"],"names":[],"mappings":"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","sourcesContent":["// Finding an email again from what was kept about it: the rules, as pure\n// functions. The search itself is an ordinary searchInboxByFilter per mailbox;\n// what lives here is how narrow that search can be made, and which of its\n// results is the email.\n//\n// The rules never guess. Every field the description gives has to agree, and at\n// least two have to be given, because a subject alone names every message in a\n// thread. A miss stays a miss rather than returning somebody else's email.\nimport { threadSubject } from '../mail';\nimport { InvalidRequestError } from '../errors';\nimport type { InboxSearchMatch } from '../types';\n\n/** The fields a match is judged on, trimmed; blank means not given. */\nexport interface WantedEmail {\n readonly subject?: string;\n readonly sender?: string;\n readonly receivedTime?: string;\n}\n\n/** `yyyy-MM-dd`, optionally followed by ` HH:mm` — how both platforms stamp a received time. */\nconst STAMP = /^\\d{4}-\\d{2}-\\d{2}([ T]\\d{2}:\\d{2})?/;\n\n/** An address inside a sender string, which may be a name, an address, or both. */\nconst ADDRESS = /[^\\s<>,;\"']+@[^\\s<>,;\"']+/;\n\nconst DAY_MS = 86_400_000;\n\nconst subjectKey = (subject: string): string => threadSubject(subject).toLowerCase();\n\n/**\n * The stamp in the listings' own form (`yyyy-MM-dd HH:mm`), or '' when it isn't\n * a stamp at all — which is then treated as not given, since it can't disagree\n * with anything.\n */\nfunction stampOf(receivedTime: string | undefined): string {\n const match = STAMP.exec((receivedTime ?? '').trim());\n return match ? match[0].replace('T', ' ') : '';\n}\n\n/** How many of the three fields the description actually gives. */\nfunction recordedCount(wanted: WantedEmail): number {\n return (wanted.subject ? 1 : 0) + (wanted.sender ? 1 : 0) + (stampOf(wanted.receivedTime) ? 1 : 0);\n}\n\n/** INVALID_REQUEST unless the description gives enough to tell one email from its thread. */\nexport function requireDescribed(wanted: WantedEmail): void {\n if (recordedCount(wanted) < 2) {\n throw new InvalidRequestError(\n 'Describe the email by at least two of subject, sender and receivedTime (as yyyy-MM-dd HH:mm): '\n + 'a subject alone names every message in its thread.',\n );\n }\n}\n\n/**\n * The subject glob to prefilter a search with: the longest run of plain\n * characters in the subject, or undefined when there's no run of four.\n *\n * A subject can hold an apostrophe or a `*`, and the prefilter becomes a query\n * on some stores; it only has to be selective, since every result is judged on\n * the whole subject afterwards.\n */\nexport function subjectPrefilter(subject: string | undefined): string | undefined {\n const longest = (threadSubject(subject ?? '').match(/[A-Za-z0-9 ._-]+/g) ?? [])\n .map(run => run.trim())\n .sort((a, b) => b.length - a.length)[0] ?? '';\n return longest.length >= 4 ? `*${longest}*` : undefined;\n}\n\n/**\n * How far back a search has to reach to cover the day the email arrived, or\n * `fallback` when no day is known. Two days of slack cover whichever timezone\n * the stamp was written in, and a search started either side of midnight.\n */\nexport function daysBackFor(receivedTime: string | undefined, fallback: number, now = Date.now()): number {\n const stamp = stampOf(receivedTime);\n if (!stamp) return fallback;\n const day = Date.parse(`${stamp.slice(0, 10)}T00:00:00Z`);\n if (Number.isNaN(day)) return fallback;\n return Math.max(1, Math.floor((now - day) / DAY_MS) + 2);\n}\n\n/** Whether a found email came from the sender described. */\nfunction senderMatches(wanted: string, match: InboxSearchMatch): boolean {\n const want = wanted.toLowerCase();\n const address = ADDRESS.exec(want)?.[0];\n if (address) return address === match.senderEmail.trim().toLowerCase();\n const name = match.senderName.trim().toLowerCase();\n return name !== '' && (want.includes(name) || name.includes(want));\n}\n\n/**\n * How well a found email answers the description, or 0 for \"not this one\".\n *\n * Disagreement on any given field disqualifies outright. Agreement is then\n * ranked, so an exact minute beats the same day: a sender who wrote twice in\n * one afternoon still resolves to the email described.\n */\nexport function matchScore(wanted: WantedEmail, match: InboxSearchMatch): number {\n if (recordedCount(wanted) < 2) return 0;\n\n const wantSubject = wanted.subject ? subjectKey(wanted.subject) : '';\n if (wantSubject && subjectKey(match.subject) !== wantSubject) return 0;\n if (wanted.sender && !senderMatches(wanted.sender, match)) return 0;\n\n const stamp = stampOf(wanted.receivedTime);\n const found = match.receivedTime.trim();\n const sameDay = stamp !== '' && stamp.slice(0, 10) === found.slice(0, 10);\n const sameMinute = sameDay && stamp.length >= 16 && stamp.slice(0, 16) === found.slice(0, 16);\n if (stamp && !sameDay) return 0;\n\n return (wantSubject ? 1 : 0) + (wanted.sender ? 2 : 0) + (sameMinute ? 3 : sameDay ? 1 : 0);\n}\n\n/** The best match, or null when nothing agrees with the description. */\nexport function bestMatch(wanted: WantedEmail, matches: readonly InboxSearchMatch[]): InboxSearchMatch | null {\n let best: InboxSearchMatch | null = null;\n let bestScore = 0;\n for (const match of matches) {\n const score = matchScore(wanted, match);\n if (score > bestScore) {\n best = match;\n bestScore = score;\n }\n }\n return best;\n}\n\n/**\n * Accounts in the order to search them: the one a listing's folderPath names\n * first (`\\\\team@example.com\\Inbox\\…`), then the rest as given.\n */\nexport function accountSearchOrder(accounts: readonly string[], folderPath: string | undefined): string[] {\n const path = (folderPath ?? '').toLowerCase();\n if (!path) return [...accounts];\n const named = accounts.filter(account => path.includes(account.toLowerCase()));\n return [...named, ...accounts.filter(account => !named.includes(account))];\n}\n"]}
|
|
@@ -1,49 +1,59 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { ReadTemplatesRequest } from '../backend';
|
|
2
|
+
import type { TemplateFolderResult } from '../types';
|
|
3
|
+
/** The reads a template- or signature-backed reply needs. */
|
|
3
4
|
export interface ReplyBodyDeps {
|
|
4
|
-
readTemplateEmails(
|
|
5
|
+
readTemplateEmails(request: ReadTemplatesRequest): Promise<TemplateFolderResult>;
|
|
5
6
|
readOutlookSignatureHtml(name: string): Promise<string>;
|
|
6
7
|
listOutlookSignatures(): Promise<string[]>;
|
|
7
8
|
}
|
|
9
|
+
/** A reply's content, as the caller described it — already validated. */
|
|
10
|
+
export interface ReplyContent {
|
|
11
|
+
readonly account: string;
|
|
12
|
+
readonly htmlBody?: string;
|
|
13
|
+
readonly templateSubject?: string;
|
|
14
|
+
readonly templateFolder: string;
|
|
15
|
+
readonly templateSection?: string;
|
|
16
|
+
readonly templatePlaceholders?: Readonly<Record<string, string>>;
|
|
17
|
+
readonly signatureName?: string;
|
|
18
|
+
}
|
|
8
19
|
/**
|
|
9
|
-
* The content of an HTML document's `<body>`, or the input when
|
|
10
|
-
* document
|
|
11
|
-
*
|
|
12
|
-
* Both things this is used on — a signature file and a saved template — are whole
|
|
13
|
-
* HTML documents, and neither may nest its `<html>`/`<body>` inside the reply's.
|
|
20
|
+
* The content of an HTML document's `<body>`, or the input when it isn't a
|
|
21
|
+
* whole document. Signature files and saved templates are both whole documents,
|
|
22
|
+
* and neither may nest its `<html>`/`<body>` inside another email's.
|
|
14
23
|
*/
|
|
15
24
|
export declare function innerBodyHtml(html: string): string;
|
|
16
25
|
/**
|
|
17
|
-
*
|
|
18
|
-
* instead of carrying its (often large, Word-generated) HTML.
|
|
26
|
+
* A saved template's HTML, found by subject.
|
|
19
27
|
*
|
|
20
|
-
* The read is filtered by subject in the platform
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
|
|
28
|
+
* The read is filtered by subject in the platform script: unfiltered, EVERY
|
|
29
|
+
* template's full body would cross the interpreter's stdout on every reply, and
|
|
30
|
+
* a folder of Word-sized templates overruns the output limit on a call that
|
|
31
|
+
* only ever needed one of them.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveTemplateHtml(deps: ReplyBodyDeps, account: string, folder: string, subject: string): Promise<string>;
|
|
34
|
+
/** A named signature's HTML, ready to insert into a body; NOT_FOUND naming the ones there are. */
|
|
35
|
+
export declare function resolveSignatureHtml(deps: Pick<ReplyBodyDeps, 'readOutlookSignatureHtml' | 'listOutlookSignatures'>, name: string): Promise<string>;
|
|
36
|
+
/**
|
|
37
|
+
* Sign a body: in its `{{SIGNATURE}}` placeholder when it has one, otherwise at
|
|
38
|
+
* the end, where Outlook itself would put a signature. In a whole document the
|
|
39
|
+
* end is just inside `</body>`, since anything after `</html>` is outside the
|
|
40
|
+
* email.
|
|
25
41
|
*/
|
|
26
|
-
export declare function
|
|
42
|
+
export declare function placeSignature(html: string, signature: string): string;
|
|
27
43
|
/**
|
|
28
44
|
* The HTML to insert above the quoted original: template resolved, section
|
|
29
|
-
*
|
|
45
|
+
* kept, placeholders and signature filled, document wrapper removed.
|
|
30
46
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
47
|
+
* A body taken from a template is always put through composition, even with
|
|
48
|
+
* nothing to fill, so a stray [[SECTION]] marker is caught here rather than
|
|
49
|
+
* mailed out.
|
|
34
50
|
*/
|
|
35
|
-
export declare function composeReplyHtml(
|
|
51
|
+
export declare function composeReplyHtml(content: ReplyContent, deps: ReplyBodyDeps): Promise<string>;
|
|
36
52
|
/**
|
|
37
|
-
* Where the new text goes in an Outlook-built reply
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* Outlook did not build that way falls back to just inside `<body>`.
|
|
43
|
-
*
|
|
44
|
-
* The insertion itself cannot live here — the reply HTML never leaves Outlook,
|
|
45
|
-
* so each platform splices it in its own language (`windows/send.ts`, and the
|
|
46
|
-
* `insertAboveQuoted` handler in `mac/run.ts`). The anchor both of them look for
|
|
47
|
-
* is the one part they must agree on, so it is stated once, here.
|
|
53
|
+
* Where the new text goes in an Outlook-built reply: straight after the opening
|
|
54
|
+
* of `WordSection1`, the div holding the empty paragraph where a person's cursor
|
|
55
|
+
* would be. A reply Outlook didn't build that way falls back to just inside
|
|
56
|
+
* `<body>`. The insertion itself happens in each platform's own language, since
|
|
57
|
+
* the reply HTML never leaves Outlook; this anchor is the part both must agree on.
|
|
48
58
|
*/
|
|
49
59
|
export declare const WORD_SECTION_ANCHOR = "WordSection1>";
|
package/dist/shared/replyBody.js
CHANGED
|
@@ -2,112 +2,125 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WORD_SECTION_ANCHOR = void 0;
|
|
4
4
|
exports.innerBodyHtml = innerBodyHtml;
|
|
5
|
-
exports.
|
|
5
|
+
exports.resolveTemplateHtml = resolveTemplateHtml;
|
|
6
|
+
exports.resolveSignatureHtml = resolveSignatureHtml;
|
|
7
|
+
exports.placeSignature = placeSignature;
|
|
6
8
|
exports.composeReplyHtml = composeReplyHtml;
|
|
7
|
-
// Everything
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
// ReplyEmailParams contract Windows does instead of a subset of it.
|
|
14
|
-
const outlookTemplateSections_1 = require("../outlookTemplateSections");
|
|
9
|
+
// Everything a reply needs before it touches Outlook: resolving a named
|
|
10
|
+
// template, keeping its section, filling placeholders and a named signature,
|
|
11
|
+
// and reducing the result to HTML that can be inserted into another document.
|
|
12
|
+
// Signing a new email uses the signature half of it. String work plus three
|
|
13
|
+
// reads each platform already provides, so it is done once here for both.
|
|
14
|
+
const templateBody_1 = require("../templateBody");
|
|
15
15
|
const errors_1 = require("../errors");
|
|
16
16
|
/**
|
|
17
|
-
* The content of an HTML document's `<body>`, or the input when
|
|
18
|
-
* document
|
|
19
|
-
*
|
|
20
|
-
* Both things this is used on — a signature file and a saved template — are whole
|
|
21
|
-
* HTML documents, and neither may nest its `<html>`/`<body>` inside the reply's.
|
|
17
|
+
* The content of an HTML document's `<body>`, or the input when it isn't a
|
|
18
|
+
* whole document. Signature files and saved templates are both whole documents,
|
|
19
|
+
* and neither may nest its `<html>`/`<body>` inside another email's.
|
|
22
20
|
*/
|
|
23
21
|
function innerBodyHtml(html) {
|
|
24
22
|
const match = /<body[^>]*>([\s\S]*)<\/body>/i.exec(html);
|
|
25
23
|
return (match ? match[1] : html).trim();
|
|
26
24
|
}
|
|
27
25
|
/**
|
|
28
|
-
*
|
|
29
|
-
* instead of carrying its (often large, Word-generated) HTML.
|
|
26
|
+
* A saved template's HTML, found by subject.
|
|
30
27
|
*
|
|
31
|
-
* The read is filtered by subject in the platform
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* needed more than one body.
|
|
28
|
+
* The read is filtered by subject in the platform script: unfiltered, EVERY
|
|
29
|
+
* template's full body would cross the interpreter's stdout on every reply, and
|
|
30
|
+
* a folder of Word-sized templates overruns the output limit on a call that
|
|
31
|
+
* only ever needed one of them.
|
|
36
32
|
*/
|
|
37
|
-
async function
|
|
38
|
-
const result = await deps.readTemplateEmails(
|
|
33
|
+
async function resolveTemplateHtml(deps, account, folder, subject) {
|
|
34
|
+
const result = await deps.readTemplateEmails({ account, folder, limit: 50, includeBody: true, subject });
|
|
39
35
|
if (!result.folderFound) {
|
|
40
36
|
const folders = result.availableFolders.join(', ') || '(none)';
|
|
41
|
-
throw new errors_1.NotFoundError('folder', `Template folder '${
|
|
37
|
+
throw new errors_1.NotFoundError('folder', `Template folder '${folder}' not found in ${account}. Top-level folders: ${folders}.`);
|
|
42
38
|
}
|
|
43
|
-
const
|
|
44
|
-
const match = result.templates.find(t => t.subject.trim().toLowerCase() ===
|
|
39
|
+
const wanted = subject.trim().toLowerCase();
|
|
40
|
+
const match = result.templates.find(t => t.subject.trim().toLowerCase() === wanted);
|
|
45
41
|
if (!match) {
|
|
46
|
-
// The read above was filtered, so it can't name the alternatives.
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
// the filter.
|
|
50
|
-
const all = await deps.readTemplateEmails(emailAccount, folderName, 50, false);
|
|
42
|
+
// The read above was filtered, so it can't name the alternatives. A
|
|
43
|
+
// second read without bodies can, cheaply.
|
|
44
|
+
const all = await deps.readTemplateEmails({ account, folder, limit: 200, includeBody: false });
|
|
51
45
|
const names = all.templates.map(t => t.subject).filter(Boolean).join(', ') || '(none)';
|
|
52
|
-
throw new errors_1.NotFoundError('template', `Template '${subject}' not found in '${
|
|
46
|
+
throw new errors_1.NotFoundError('template', `Template '${subject}' not found in '${folder}'. Templates there: ${names}.`);
|
|
53
47
|
}
|
|
54
|
-
if (
|
|
48
|
+
if (match.htmlBody.trim() === '') {
|
|
55
49
|
throw new errors_1.NotFoundError('template', `Template '${subject}' has an empty HTML body.`);
|
|
56
50
|
}
|
|
57
51
|
return match.htmlBody;
|
|
58
52
|
}
|
|
53
|
+
/** Where a template asks for the signature. */
|
|
54
|
+
const SIGNATURE_TOKEN = '{{SIGNATURE}}';
|
|
55
|
+
/** A named signature's HTML, ready to insert into a body; NOT_FOUND naming the ones there are. */
|
|
56
|
+
async function resolveSignatureHtml(deps, name) {
|
|
57
|
+
const signatureHtml = await deps.readOutlookSignatureHtml(name);
|
|
58
|
+
if (signatureHtml.trim() === '') {
|
|
59
|
+
const available = (await deps.listOutlookSignatures()).join(', ') || '(none)';
|
|
60
|
+
throw new errors_1.NotFoundError('signature', `Outlook signature '${name}' not found. Signatures on this machine: ${available}.`);
|
|
61
|
+
}
|
|
62
|
+
return innerBodyHtml(signatureHtml);
|
|
63
|
+
}
|
|
64
|
+
/** Whether a body says where its signature goes. */
|
|
65
|
+
function asksForSignature(html) {
|
|
66
|
+
return (0, templateBody_1.findTokens)(html, [SIGNATURE_TOKEN]).length > 0;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Sign a body: in its `{{SIGNATURE}}` placeholder when it has one, otherwise at
|
|
70
|
+
* the end, where Outlook itself would put a signature. In a whole document the
|
|
71
|
+
* end is just inside `</body>`, since anything after `</html>` is outside the
|
|
72
|
+
* email.
|
|
73
|
+
*/
|
|
74
|
+
function placeSignature(html, signature) {
|
|
75
|
+
if (asksForSignature(html))
|
|
76
|
+
return (0, templateBody_1.replaceToken)(html, SIGNATURE_TOKEN, signature);
|
|
77
|
+
const block = `<br>${signature}`;
|
|
78
|
+
const close = html.toLowerCase().lastIndexOf('</body');
|
|
79
|
+
return close === -1 ? html + block : html.slice(0, close) + block + html.slice(close);
|
|
80
|
+
}
|
|
59
81
|
/**
|
|
60
82
|
* The HTML to insert above the quoted original: template resolved, section
|
|
61
|
-
*
|
|
83
|
+
* kept, placeholders and signature filled, document wrapper removed.
|
|
62
84
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
85
|
+
* A body taken from a template is always put through composition, even with
|
|
86
|
+
* nothing to fill, so a stray [[SECTION]] marker is caught here rather than
|
|
87
|
+
* mailed out.
|
|
66
88
|
*/
|
|
67
|
-
async function composeReplyHtml(
|
|
68
|
-
let
|
|
89
|
+
async function composeReplyHtml(content, deps) {
|
|
90
|
+
let html = content.htmlBody;
|
|
69
91
|
let fromTemplate = false;
|
|
70
|
-
if ((!
|
|
71
|
-
|
|
92
|
+
if ((!html || html.trim() === '') && content.templateSubject) {
|
|
93
|
+
html = await resolveTemplateHtml(deps, content.account, content.templateFolder, content.templateSubject);
|
|
72
94
|
fromTemplate = true;
|
|
73
95
|
}
|
|
74
|
-
if (!
|
|
75
|
-
throw new errors_1.InvalidRequestError('
|
|
76
|
-
}
|
|
77
|
-
// A named signature fills {{SIGNATURE}} like any other placeholder, but is
|
|
78
|
-
// read here so the caller never carries the signature HTML — images and all —
|
|
79
|
-
// across the wire.
|
|
80
|
-
let placeholders = params.templatePlaceholders;
|
|
81
|
-
if (params.signatureName) {
|
|
82
|
-
const signatureHtml = await deps.readOutlookSignatureHtml(params.signatureName);
|
|
83
|
-
if (signatureHtml.trim() === '') {
|
|
84
|
-
const available = (await deps.listOutlookSignatures()).join(', ') || '(none)';
|
|
85
|
-
throw new errors_1.NotFoundError('signature', `Outlook signature '${params.signatureName}' not found. Available signatures: ${available}.`);
|
|
86
|
-
}
|
|
87
|
-
placeholders = { ...placeholders, SIGNATURE: innerBodyHtml(signatureHtml) };
|
|
96
|
+
if (!html || html.trim() === '') {
|
|
97
|
+
throw new errors_1.InvalidRequestError('A reply needs either htmlBody or templateSubject.');
|
|
88
98
|
}
|
|
99
|
+
// A signature with a placeholder to go in is filled with the others, so
|
|
100
|
+
// one that sat in a dropped section goes with it. Without one it is added
|
|
101
|
+
// below the new text once everything else is composed.
|
|
102
|
+
let placeholders = content.templatePlaceholders;
|
|
103
|
+
const signature = content.signatureName ? await resolveSignatureHtml(deps, content.signatureName) : '';
|
|
104
|
+
const signatureInPlace = signature !== '' && asksForSignature(html);
|
|
105
|
+
if (signatureInPlace)
|
|
106
|
+
placeholders = { ...placeholders, SIGNATURE: signature };
|
|
89
107
|
const hasPlaceholders = !!placeholders && Object.keys(placeholders).length > 0;
|
|
90
|
-
if (fromTemplate ||
|
|
91
|
-
|
|
92
|
-
section:
|
|
108
|
+
if (fromTemplate || content.templateSection || hasPlaceholders) {
|
|
109
|
+
html = (0, templateBody_1.composeTemplateBody)(html, {
|
|
110
|
+
section: content.templateSection,
|
|
93
111
|
placeholders,
|
|
94
|
-
label:
|
|
112
|
+
label: content.templateSubject,
|
|
95
113
|
});
|
|
96
114
|
}
|
|
97
|
-
|
|
115
|
+
const body = innerBodyHtml(html);
|
|
116
|
+
return signature !== '' && !signatureInPlace ? placeSignature(body, signature) : body;
|
|
98
117
|
}
|
|
99
118
|
/**
|
|
100
|
-
* Where the new text goes in an Outlook-built reply
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* Outlook did not build that way falls back to just inside `<body>`.
|
|
106
|
-
*
|
|
107
|
-
* The insertion itself cannot live here — the reply HTML never leaves Outlook,
|
|
108
|
-
* so each platform splices it in its own language (`windows/send.ts`, and the
|
|
109
|
-
* `insertAboveQuoted` handler in `mac/run.ts`). The anchor both of them look for
|
|
110
|
-
* is the one part they must agree on, so it is stated once, here.
|
|
119
|
+
* Where the new text goes in an Outlook-built reply: straight after the opening
|
|
120
|
+
* of `WordSection1`, the div holding the empty paragraph where a person's cursor
|
|
121
|
+
* would be. A reply Outlook didn't build that way falls back to just inside
|
|
122
|
+
* `<body>`. The insertion itself happens in each platform's own language, since
|
|
123
|
+
* the reply HTML never leaves Outlook; this anchor is the part both must agree on.
|
|
111
124
|
*/
|
|
112
125
|
exports.WORD_SECTION_ANCHOR = 'WordSection1>';
|
|
113
126
|
//# sourceMappingURL=replyBody.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replyBody.js","sourceRoot":"","sources":["../../src/shared/replyBody.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"replyBody.js","sourceRoot":"","sources":["../../src/shared/replyBody.ts"],"names":[],"mappings":"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","sourcesContent":["// Everything a reply needs before it touches Outlook: resolving a named\n// template, keeping its section, filling placeholders and a named signature,\n// and reducing the result to HTML that can be inserted into another document.\n// Signing a new email uses the signature half of it. String work plus three\n// reads each platform already provides, so it is done once here for both.\nimport { composeTemplateBody, findTokens, replaceToken } from '../templateBody';\nimport { InvalidRequestError, NotFoundError } from '../errors';\nimport type { ReadTemplatesRequest } from '../backend';\nimport type { TemplateFolderResult } from '../types';\n\n/** The reads a template- or signature-backed reply needs. */\nexport interface ReplyBodyDeps {\n readTemplateEmails(request: ReadTemplatesRequest): Promise<TemplateFolderResult>;\n\n readOutlookSignatureHtml(name: string): Promise<string>;\n\n listOutlookSignatures(): Promise<string[]>;\n}\n\n/** A reply's content, as the caller described it — already validated. */\nexport interface ReplyContent {\n readonly account: string;\n readonly htmlBody?: string;\n readonly templateSubject?: string;\n readonly templateFolder: string;\n readonly templateSection?: string;\n readonly templatePlaceholders?: Readonly<Record<string, string>>;\n readonly signatureName?: string;\n}\n\n/**\n * The content of an HTML document's `<body>`, or the input when it isn't a\n * whole document. Signature files and saved templates are both whole documents,\n * and neither may nest its `<html>`/`<body>` inside another email's.\n */\nexport function innerBodyHtml(html: string): string {\n const match = /<body[^>]*>([\\s\\S]*)<\\/body>/i.exec(html);\n return (match ? match[1] : html).trim();\n}\n\n/**\n * A saved template's HTML, found by subject.\n *\n * The read is filtered by subject in the platform script: unfiltered, EVERY\n * template's full body would cross the interpreter's stdout on every reply, and\n * a folder of Word-sized templates overruns the output limit on a call that\n * only ever needed one of them.\n */\nexport async function resolveTemplateHtml(\n deps: ReplyBodyDeps,\n account: string,\n folder: string,\n subject: string,\n): Promise<string> {\n const result = await deps.readTemplateEmails({account, folder, limit: 50, includeBody: true, subject});\n if (!result.folderFound) {\n const folders = result.availableFolders.join(', ') || '(none)';\n throw new NotFoundError('folder', `Template folder '${folder}' not found in ${account}. Top-level folders: ${folders}.`);\n }\n const wanted = subject.trim().toLowerCase();\n const match = result.templates.find(t => t.subject.trim().toLowerCase() === wanted);\n if (!match) {\n // The read above was filtered, so it can't name the alternatives. A\n // second read without bodies can, cheaply.\n const all = await deps.readTemplateEmails({account, folder, limit: 200, includeBody: false});\n const names = all.templates.map(t => t.subject).filter(Boolean).join(', ') || '(none)';\n throw new NotFoundError('template', `Template '${subject}' not found in '${folder}'. Templates there: ${names}.`);\n }\n if (match.htmlBody.trim() === '') {\n throw new NotFoundError('template', `Template '${subject}' has an empty HTML body.`);\n }\n return match.htmlBody;\n}\n\n/** Where a template asks for the signature. */\nconst SIGNATURE_TOKEN = '{{SIGNATURE}}';\n\n/** A named signature's HTML, ready to insert into a body; NOT_FOUND naming the ones there are. */\nexport async function resolveSignatureHtml(\n deps: Pick<ReplyBodyDeps, 'readOutlookSignatureHtml' | 'listOutlookSignatures'>,\n name: string,\n): Promise<string> {\n const signatureHtml = await deps.readOutlookSignatureHtml(name);\n if (signatureHtml.trim() === '') {\n const available = (await deps.listOutlookSignatures()).join(', ') || '(none)';\n throw new NotFoundError('signature', `Outlook signature '${name}' not found. Signatures on this machine: ${available}.`);\n }\n return innerBodyHtml(signatureHtml);\n}\n\n/** Whether a body says where its signature goes. */\nfunction asksForSignature(html: string): boolean {\n return findTokens(html, [SIGNATURE_TOKEN]).length > 0;\n}\n\n/**\n * Sign a body: in its `{{SIGNATURE}}` placeholder when it has one, otherwise at\n * the end, where Outlook itself would put a signature. In a whole document the\n * end is just inside `</body>`, since anything after `</html>` is outside the\n * email.\n */\nexport function placeSignature(html: string, signature: string): string {\n if (asksForSignature(html)) return replaceToken(html, SIGNATURE_TOKEN, signature);\n const block = `<br>${signature}`;\n const close = html.toLowerCase().lastIndexOf('</body');\n return close === -1 ? html + block : html.slice(0, close) + block + html.slice(close);\n}\n\n/**\n * The HTML to insert above the quoted original: template resolved, section\n * kept, placeholders and signature filled, document wrapper removed.\n *\n * A body taken from a template is always put through composition, even with\n * nothing to fill, so a stray [[SECTION]] marker is caught here rather than\n * mailed out.\n */\nexport async function composeReplyHtml(content: ReplyContent, deps: ReplyBodyDeps): Promise<string> {\n let html = content.htmlBody;\n let fromTemplate = false;\n if ((!html || html.trim() === '') && content.templateSubject) {\n html = await resolveTemplateHtml(deps, content.account, content.templateFolder, content.templateSubject);\n fromTemplate = true;\n }\n if (!html || html.trim() === '') {\n throw new InvalidRequestError('A reply needs either htmlBody or templateSubject.');\n }\n\n // A signature with a placeholder to go in is filled with the others, so\n // one that sat in a dropped section goes with it. Without one it is added\n // below the new text once everything else is composed.\n let placeholders = content.templatePlaceholders;\n const signature = content.signatureName ? await resolveSignatureHtml(deps, content.signatureName) : '';\n const signatureInPlace = signature !== '' && asksForSignature(html);\n if (signatureInPlace) placeholders = {...placeholders, SIGNATURE: signature};\n\n const hasPlaceholders = !!placeholders && Object.keys(placeholders).length > 0;\n if (fromTemplate || content.templateSection || hasPlaceholders) {\n html = composeTemplateBody(html, {\n section: content.templateSection,\n placeholders,\n label: content.templateSubject,\n });\n }\n const body = innerBodyHtml(html);\n return signature !== '' && !signatureInPlace ? placeSignature(body, signature) : body;\n}\n\n/**\n * Where the new text goes in an Outlook-built reply: straight after the opening\n * of `WordSection1`, the div holding the empty paragraph where a person's cursor\n * would be. A reply Outlook didn't build that way falls back to just inside\n * `<body>`. The insertion itself happens in each platform's own language, since\n * the reply HTML never leaves Outlook; this anchor is the part both must agree on.\n */\nexport const WORD_SECTION_ANCHOR = 'WordSection1>';\n"]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sections and placeholders in HTML email templates.
|
|
3
|
+
*
|
|
4
|
+
* A template email is an ordinary saved mail its owner edits in Outlook, so the
|
|
5
|
+
* greeting, closing and signature stay theirs. Where one template serves several
|
|
6
|
+
* kinds of reply, the variants live inside it between markers:
|
|
7
|
+
*
|
|
8
|
+
* Hello,
|
|
9
|
+
* [[ACCEPT]] Thank you — we accept... [[/ACCEPT]]
|
|
10
|
+
* [[DECLINE]] Thank you for letting us know... [[/DECLINE]]
|
|
11
|
+
* [[CLARIFY]] {{QUESTIONS}} [[/CLARIFY]]
|
|
12
|
+
* Best regards, {{SIGNATURE}}
|
|
13
|
+
*
|
|
14
|
+
* Composing keeps the requested section, drops the others, and substitutes the
|
|
15
|
+
* `{{PLACEHOLDER}}`s — so shared wording is written once and a caller names a
|
|
16
|
+
* section instead of carrying the whole body.
|
|
17
|
+
*
|
|
18
|
+
* Everything works on the raw HTML string rather than a parsed tree: Word's
|
|
19
|
+
* markup carries conditional comments and namespaced tags a parser round-trip
|
|
20
|
+
* would rewrite, and the composed body must stay byte-identical to what was
|
|
21
|
+
* written.
|
|
22
|
+
*/
|
|
23
|
+
export interface TemplateMarkers {
|
|
24
|
+
/** Section names with both an opening and a closing marker, in document order. */
|
|
25
|
+
sections: string[];
|
|
26
|
+
/** `{{NAME}}` placeholders found anywhere in the template. */
|
|
27
|
+
placeholders: string[];
|
|
28
|
+
}
|
|
29
|
+
/** Find the markers in a template body (HTML, or the plain text Outlook derives from it). */
|
|
30
|
+
export declare function findTemplateMarkers(body: string): TemplateMarkers;
|
|
31
|
+
/** Replace every occurrence of `token` with `value`, which is never rescanned. */
|
|
32
|
+
export declare function replaceToken(html: string, token: string, value: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Remove `token` together with the line it sits on — the table row or
|
|
35
|
+
* paragraph, label included. For an optional detail this is the difference
|
|
36
|
+
* between the email not mentioning it and a heading with nothing under it.
|
|
37
|
+
* Falls back to deleting just the token when it isn't inside a line element.
|
|
38
|
+
*/
|
|
39
|
+
export declare function removeTokenLine(html: string, token: string): string;
|
|
40
|
+
/** Which of `tokens` the template actually uses — what the caller has to supply. */
|
|
41
|
+
export declare function findTokens(html: string, tokens: readonly string[]): string[];
|
|
42
|
+
/**
|
|
43
|
+
* `{Token}`-shaped text still in a filled-in body: a placeholder misspelled
|
|
44
|
+
* while editing, or one nothing supplied. Reads visible text only, so the
|
|
45
|
+
* braces of a <style> block are not mistaken for placeholders. Catches a
|
|
46
|
+
* leftover `{{TOKEN}}` too, reported by its inner `{TOKEN}`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function findUnfilledTokens(html: string): string[];
|
|
49
|
+
export interface ComposeOptions {
|
|
50
|
+
/** Section to keep; the other sections are dropped with their markers. */
|
|
51
|
+
section?: string;
|
|
52
|
+
/** `{{NAME}}` → HTML to substitute. Names match case-insensitively. */
|
|
53
|
+
placeholders?: Readonly<Record<string, string>>;
|
|
54
|
+
/** The template's name, for error messages. */
|
|
55
|
+
label?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Reduce a template body to the one email that goes out: keep `section`, drop
|
|
59
|
+
* the rest, fill the placeholders. Throws INVALID_REQUEST rather than guessing —
|
|
60
|
+
* a template edited into an unusable state has to stop the send, not deliver a
|
|
61
|
+
* body with `[[ACCEPT]]` or an unfilled placeholder in it.
|
|
62
|
+
*/
|
|
63
|
+
export declare function composeTemplateBody(html: string, options?: ComposeOptions): string;
|