@dawnlit/outlook-bridge 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/backend.d.ts +2 -2
- package/dist/backend.js.map +1 -1
- package/dist/bridge.js +3 -3
- package/dist/bridge.js.map +1 -1
- package/dist/errors.d.ts +0 -2
- package/dist/errors.js +1 -6
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/mac/accounts.js.map +1 -1
- package/dist/mac/attachments.js +1 -1
- package/dist/mac/attachments.js.map +1 -1
- package/dist/mac/bounces.js +8 -12
- package/dist/mac/bounces.js.map +1 -1
- package/dist/mac/cleanup.js +4 -41
- package/dist/mac/cleanup.js.map +1 -1
- package/dist/mac/drafts.d.ts +3 -1
- package/dist/mac/drafts.js +34 -33
- package/dist/mac/drafts.js.map +1 -1
- package/dist/mac/folders.d.ts +5 -3
- package/dist/mac/folders.js +18 -19
- package/dist/mac/folders.js.map +1 -1
- package/dist/mac/profile.js.map +1 -1
- package/dist/mac/read.d.ts +4 -0
- package/dist/mac/read.js +54 -16
- package/dist/mac/read.js.map +1 -1
- package/dist/mac/run.d.ts +19 -2
- package/dist/mac/run.js +42 -2
- package/dist/mac/run.js.map +1 -1
- package/dist/mac/scripts.d.ts +21 -14
- package/dist/mac/scripts.js +52 -21
- package/dist/mac/scripts.js.map +1 -1
- package/dist/mac/send.d.ts +3 -3
- package/dist/mac/send.js +44 -12
- package/dist/mac/send.js.map +1 -1
- package/dist/mac/templates.js +15 -18
- package/dist/mac/templates.js.map +1 -1
- package/dist/mail.d.ts +5 -3
- package/dist/mail.js +19 -6
- package/dist/mail.js.map +1 -1
- package/dist/runtime.js.map +1 -1
- package/dist/shared/args.js.map +1 -1
- package/dist/shared/attachmentMatch.d.ts +0 -6
- package/dist/shared/attachmentMatch.js +0 -1
- package/dist/shared/attachmentMatch.js.map +1 -1
- package/dist/shared/exec.js.map +1 -1
- package/dist/shared/json.d.ts +11 -6
- package/dist/shared/json.js +12 -7
- package/dist/shared/json.js.map +1 -1
- package/dist/shared/locate.d.ts +0 -8
- package/dist/shared/locate.js +0 -1
- package/dist/shared/locate.js.map +1 -1
- package/dist/shared/replyBody.d.ts +0 -15
- package/dist/shared/replyBody.js +0 -2
- package/dist/shared/replyBody.js.map +1 -1
- package/dist/shared/templateLimits.d.ts +14 -0
- package/dist/shared/templateLimits.js +21 -0
- package/dist/shared/templateLimits.js.map +1 -0
- package/dist/templateBody.d.ts +6 -0
- package/dist/templateBody.js +8 -1
- package/dist/templateBody.js.map +1 -1
- package/dist/types.d.ts +23 -3
- package/dist/types.js.map +1 -1
- package/dist/windows/bounces.js +4 -4
- package/dist/windows/bounces.js.map +1 -1
- package/dist/windows/cleanup.d.ts +5 -1
- package/dist/windows/cleanup.js +16 -11
- package/dist/windows/cleanup.js.map +1 -1
- package/dist/windows/drafts.d.ts +4 -4
- package/dist/windows/drafts.js +20 -31
- package/dist/windows/drafts.js.map +1 -1
- package/dist/windows/folders.js +1 -1
- package/dist/windows/folders.js.map +1 -1
- package/dist/windows/read.js +6 -2
- package/dist/windows/read.js.map +1 -1
- package/dist/windows/run.d.ts +0 -2
- package/dist/windows/run.js +1 -2
- package/dist/windows/run.js.map +1 -1
- package/dist/windows/scripts.d.ts +4 -4
- package/dist/windows/scripts.js +10 -10
- package/dist/windows/scripts.js.map +1 -1
- package/dist/windows/send.d.ts +8 -3
- package/dist/windows/send.js +10 -2
- package/dist/windows/send.js.map +1 -1
- package/dist/windows/templates.js +9 -15
- package/dist/windows/templates.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +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
|
+
{"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 */\nfunction 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"]}
|
|
@@ -16,21 +16,6 @@ export interface ReplyContent {
|
|
|
16
16
|
readonly templatePlaceholders?: Readonly<Record<string, string>>;
|
|
17
17
|
readonly signatureName?: string;
|
|
18
18
|
}
|
|
19
|
-
/**
|
|
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.
|
|
23
|
-
*/
|
|
24
|
-
export declare function innerBodyHtml(html: string): string;
|
|
25
|
-
/**
|
|
26
|
-
* A saved template's HTML, found by subject.
|
|
27
|
-
*
|
|
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
19
|
/** A named signature's HTML, ready to insert into a body; NOT_FOUND naming the ones there are. */
|
|
35
20
|
export declare function resolveSignatureHtml(deps: Pick<ReplyBodyDeps, 'readOutlookSignatureHtml' | 'listOutlookSignatures'>, name: string): Promise<string>;
|
|
36
21
|
/**
|
package/dist/shared/replyBody.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WORD_SECTION_ANCHOR = void 0;
|
|
4
|
-
exports.innerBodyHtml = innerBodyHtml;
|
|
5
|
-
exports.resolveTemplateHtml = resolveTemplateHtml;
|
|
6
4
|
exports.resolveSignatureHtml = resolveSignatureHtml;
|
|
7
5
|
exports.placeSignature = placeSignature;
|
|
8
6
|
exports.composeReplyHtml = composeReplyHtml;
|
|
@@ -1 +1 @@
|
|
|
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"]}
|
|
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 */\nfunction 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 */\nasync 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,14 @@
|
|
|
1
|
+
/** How deep under the mailbox root a template folder is searched for. */
|
|
2
|
+
export declare const TEMPLATE_SEARCH_DEPTH = 3;
|
|
3
|
+
/** Cap on one template body; a Word-generated one is large, and a runaway one shouldn't sink the call. */
|
|
4
|
+
export declare const MAX_TEMPLATE_HTML = 100000;
|
|
5
|
+
/** Characters of plain text standing in for a body that wasn't asked for. */
|
|
6
|
+
export declare const TEMPLATE_PREVIEW_CHARS = 200;
|
|
7
|
+
/**
|
|
8
|
+
* An embedded (cid:) image tag, removed from a template body as it is read.
|
|
9
|
+
* Those images live as attachments on the TEMPLATE item, so its HTML reused
|
|
10
|
+
* elsewhere would show broken-image placeholders; template emails are text and
|
|
11
|
+
* markup only. Matched ignoring case on both platforms, since editors write
|
|
12
|
+
* both `<img src=` and `<IMG SRC=`.
|
|
13
|
+
*/
|
|
14
|
+
export declare const EMBEDDED_IMAGE_SOURCE = "<img[^>]*src=\"cid:[^\"]*\"[^>]*>";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// How far both platforms go when reading a folder of template emails, and what
|
|
3
|
+
// they strip from a body, stated once so a template that reads one way on
|
|
4
|
+
// Windows can't read another on macOS.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.EMBEDDED_IMAGE_SOURCE = exports.TEMPLATE_PREVIEW_CHARS = exports.MAX_TEMPLATE_HTML = exports.TEMPLATE_SEARCH_DEPTH = void 0;
|
|
7
|
+
/** How deep under the mailbox root a template folder is searched for. */
|
|
8
|
+
exports.TEMPLATE_SEARCH_DEPTH = 3;
|
|
9
|
+
/** Cap on one template body; a Word-generated one is large, and a runaway one shouldn't sink the call. */
|
|
10
|
+
exports.MAX_TEMPLATE_HTML = 100_000;
|
|
11
|
+
/** Characters of plain text standing in for a body that wasn't asked for. */
|
|
12
|
+
exports.TEMPLATE_PREVIEW_CHARS = 200;
|
|
13
|
+
/**
|
|
14
|
+
* An embedded (cid:) image tag, removed from a template body as it is read.
|
|
15
|
+
* Those images live as attachments on the TEMPLATE item, so its HTML reused
|
|
16
|
+
* elsewhere would show broken-image placeholders; template emails are text and
|
|
17
|
+
* markup only. Matched ignoring case on both platforms, since editors write
|
|
18
|
+
* both `<img src=` and `<IMG SRC=`.
|
|
19
|
+
*/
|
|
20
|
+
exports.EMBEDDED_IMAGE_SOURCE = '<img[^>]*src="cid:[^"]*"[^>]*>';
|
|
21
|
+
//# sourceMappingURL=templateLimits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templateLimits.js","sourceRoot":"","sources":["../../src/shared/templateLimits.ts"],"names":[],"mappings":";AAAA,+EAA+E;AAC/E,0EAA0E;AAC1E,uCAAuC;;;AAEvC,yEAAyE;AAC5D,QAAA,qBAAqB,GAAG,CAAC,CAAC;AAEvC,0GAA0G;AAC7F,QAAA,iBAAiB,GAAG,OAAO,CAAC;AAEzC,6EAA6E;AAChE,QAAA,sBAAsB,GAAG,GAAG,CAAC;AAE1C;;;;;;GAMG;AACU,QAAA,qBAAqB,GAAG,gCAAgC,CAAC","sourcesContent":["// How far both platforms go when reading a folder of template emails, and what\n// they strip from a body, stated once so a template that reads one way on\n// Windows can't read another on macOS.\n\n/** How deep under the mailbox root a template folder is searched for. */\nexport const TEMPLATE_SEARCH_DEPTH = 3;\n\n/** Cap on one template body; a Word-generated one is large, and a runaway one shouldn't sink the call. */\nexport const MAX_TEMPLATE_HTML = 100_000;\n\n/** Characters of plain text standing in for a body that wasn't asked for. */\nexport const TEMPLATE_PREVIEW_CHARS = 200;\n\n/**\n * An embedded (cid:) image tag, removed from a template body as it is read.\n * Those images live as attachments on the TEMPLATE item, so its HTML reused\n * elsewhere would show broken-image placeholders; template emails are text and\n * markup only. Matched ignoring case on both platforms, since editors write\n * both `<img src=` and `<IMG SRC=`.\n */\nexport const EMBEDDED_IMAGE_SOURCE = '<img[^>]*src=\"cid:[^\"]*\"[^>]*>';\n"]}
|
package/dist/templateBody.d.ts
CHANGED
|
@@ -28,6 +28,12 @@ export interface TemplateMarkers {
|
|
|
28
28
|
}
|
|
29
29
|
/** Find the markers in a template body (HTML, or the plain text Outlook derives from it). */
|
|
30
30
|
export declare function findTemplateMarkers(body: string): TemplateMarkers;
|
|
31
|
+
/**
|
|
32
|
+
* Any `[[SECTION]]`, `[[/SECTION]]` or `{{PLACEHOLDER}}` token, paired or not, as
|
|
33
|
+
* a pattern source JavaScript and .NET read the same way: Windows scans a
|
|
34
|
+
* template's plain text with it inside PowerShell.
|
|
35
|
+
*/
|
|
36
|
+
export declare const MARKER_TOKEN_SOURCE = "\\[\\[\\s*/?\\s*[A-Za-z0-9_-]{1,40}\\s*\\]\\]|\\{\\{\\s*[A-Za-z0-9_-]{1,40}\\s*\\}\\}";
|
|
31
37
|
/** Replace every occurrence of `token` with `value`, which is never rescanned. */
|
|
32
38
|
export declare function replaceToken(html: string, token: string, value: string): string;
|
|
33
39
|
/**
|
package/dist/templateBody.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* written.
|
|
23
23
|
*/
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.MARKER_TOKEN_SOURCE = void 0;
|
|
25
26
|
exports.findTemplateMarkers = findTemplateMarkers;
|
|
26
27
|
exports.replaceToken = replaceToken;
|
|
27
28
|
exports.removeTokenLine = removeTokenLine;
|
|
@@ -105,6 +106,12 @@ function findTemplateMarkers(body) {
|
|
|
105
106
|
}
|
|
106
107
|
return { sections: opened.filter(n => closed.has(n)), placeholders };
|
|
107
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Any `[[SECTION]]`, `[[/SECTION]]` or `{{PLACEHOLDER}}` token, paired or not, as
|
|
111
|
+
* a pattern source JavaScript and .NET read the same way: Windows scans a
|
|
112
|
+
* template's plain text with it inside PowerShell.
|
|
113
|
+
*/
|
|
114
|
+
exports.MARKER_TOKEN_SOURCE = `\\[\\[\\s*/?\\s*${NAME}\\s*\\]\\]|\\{\\{\\s*${NAME}\\s*\\}\\}`;
|
|
108
115
|
/**
|
|
109
116
|
* Every `[[...]]` / `{{...}}` token still in a body, paired or not — the last
|
|
110
117
|
* check before a composed body goes out, so a half-deleted marker stops the
|
|
@@ -112,7 +119,7 @@ function findTemplateMarkers(body) {
|
|
|
112
119
|
*/
|
|
113
120
|
function markerTokens(body) {
|
|
114
121
|
const text = stripToText(body || '');
|
|
115
|
-
const re = new RegExp(
|
|
122
|
+
const re = new RegExp(exports.MARKER_TOKEN_SOURCE, 'g');
|
|
116
123
|
const found = [];
|
|
117
124
|
let m;
|
|
118
125
|
while ((m = re.exec(text)) !== null) {
|
package/dist/templateBody.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templateBody.js","sourceRoot":"","sources":["../src/templateBody.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Sections and placeholders in HTML email templates.\n *\n * A template email is an ordinary saved mail its owner edits in Outlook, so the\n * greeting, closing and signature stay theirs. Where one template serves several\n * kinds of reply, the variants live inside it between markers:\n *\n * Hello,\n * [[ACCEPT]] Thank you — we accept... [[/ACCEPT]]\n * [[DECLINE]] Thank you for letting us know... [[/DECLINE]]\n * [[CLARIFY]] {{QUESTIONS}} [[/CLARIFY]]\n * Best regards, {{SIGNATURE}}\n *\n * Composing keeps the requested section, drops the others, and substitutes the\n * `{{PLACEHOLDER}}`s — so shared wording is written once and a caller names a\n * section instead of carrying the whole body.\n *\n * Everything works on the raw HTML string rather than a parsed tree: Word's\n * markup carries conditional comments and namespaced tags a parser round-trip\n * would rewrite, and the composed body must stay byte-identical to what was\n * written.\n */\n\nimport { InvalidRequestError } from './errors';\n\n/** Marker and placeholder names: what a person can reasonably type in Outlook. */\nconst NAME = '[A-Za-z0-9_-]{1,40}';\n\n/**\n * What Word may inject *between two characters of a marker*: tags (it splits\n * runs across <span>s freely), comments, non-breaking spaces, and the newlines\n * it wraps long lines with — sometimes mid-word. So `[[ACCEPT]]` is matched\n * character by character with this between, never as a literal string.\n */\nconst GAP = '(?:<!--[\\\\s\\\\S]*?-->|<[^>]*>| | |\\\\s)*';\n\n/** Block elements a marker can sit alone inside; the whole block goes when it does. */\nconst BLOCK_TAGS = ['p', 'div', 'li'];\n\n/**\n * Block elements that make up one *line* of a template, for a token whose\n * empty value takes its whole line out (see `removeTokenLine`). Outermost\n * first: Word puts a <p> inside every table cell, so a token in a details table\n * has to drop the <tr> — dropping the <p> would leave the row's label beside a\n * blank cell.\n *\n * <div> is deliberately absent: it is rarely a line and often the wrapper around\n * the entire body, so a token directly inside one would take the whole email\n * with it. Deleting just the token is the safer miss.\n */\nconst LINE_TAGS = ['tr', 'li', 'p'];\n\nfunction escapeRe(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * Tag-tolerant matcher for one literal token, e.g. `[[/ACCEPT]]` or `{{QUESTIONS}}`.\n *\n * Memoized: the pattern interleaves `GAP` between every character, so a short\n * token compiles to hundreds of characters, and the replace loops ask for the\n * same token repeatedly while walking a template.\n */\nconst markerRegexCache = new Map<string, RegExp>();\n\nfunction markerRegex(token: string): RegExp {\n let regex = markerRegexCache.get(token);\n if (!regex) {\n regex = new RegExp(token.split('').map(escapeRe).join(GAP), 'i');\n markerRegexCache.set(token, regex);\n }\n return regex;\n}\n\n/** HTML → rough plain text, enough to spot markers and test a block for emptiness. */\nfunction stripToText(html: string): string {\n return html\n .replace(/<!--[\\s\\S]*?-->/g, '')\n .replace(/<[^>]*>/g, '')\n .replace(/ | /gi, ' ');\n}\n\nexport interface TemplateMarkers {\n /** Section names with both an opening and a closing marker, in document order. */\n sections: string[];\n /** `{{NAME}}` placeholders found anywhere in the template. */\n placeholders: string[];\n}\n\n/** Find the markers in a template body (HTML, or the plain text Outlook derives from it). */\nexport function findTemplateMarkers(body: string): TemplateMarkers {\n const text = stripToText(body || '');\n const opened: string[] = [];\n const closed = new Set<string>();\n const seen = new Set<string>();\n const sectionRe = new RegExp(`\\\\[\\\\[\\\\s*(/?)\\\\s*(${NAME})\\\\s*\\\\]\\\\]`, 'g');\n let m: RegExpExecArray | null;\n while ((m = sectionRe.exec(text)) !== null) {\n const name = m[2].toUpperCase();\n if (m[1]) {\n closed.add(name);\n } else if (!seen.has(name)) {\n seen.add(name);\n opened.push(name);\n }\n }\n const placeholders: string[] = [];\n const placeholderRe = new RegExp(`\\\\{\\\\{\\\\s*(${NAME})\\\\s*\\\\}\\\\}`, 'g');\n while ((m = placeholderRe.exec(text)) !== null) {\n const name = m[1].toUpperCase();\n if (!placeholders.includes(name)) placeholders.push(name);\n }\n return {sections: opened.filter(n => closed.has(n)), placeholders};\n}\n\n/**\n * Every `[[...]]` / `{{...}}` token still in a body, paired or not — the last\n * check before a composed body goes out, so a half-deleted marker stops the\n * send instead of reaching a recipient.\n */\nfunction markerTokens(body: string): string[] {\n const text = stripToText(body || '');\n const re = new RegExp(`\\\\[\\\\[\\\\s*/?\\\\s*${NAME}\\\\s*\\\\]\\\\]|\\\\{\\\\{\\\\s*${NAME}\\\\s*\\\\}\\\\}`, 'g');\n const found: string[] = [];\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n const token = m[0].replace(/\\s+/g, '');\n if (!found.includes(token)) found.push(token);\n }\n return found;\n}\n\ninterface Span {\n start: number;\n end: number;\n}\n\n/**\n * The nearest enclosing block element of [start, end), or null when the marker\n * isn't inside one. The closing tag is the first of the same name after the\n * marker — Word doesn't nest <p>, and where that guess is wrong the emptiness\n * test simply fails and the caller falls back to the marker's own span.\n */\nfunction enclosingBlock(html: string, start: number, end: number, tags: string[] = BLOCK_TAGS): {\n block: Span;\n contentStart: number;\n contentEnd: number;\n} | null {\n let best: { idx: number; contentStart: number; tag: string } | null = null;\n for (const tag of tags) {\n const openRe = new RegExp(`<${tag}(?:\\\\s[^>]*)?>`, 'gi');\n let m: RegExpExecArray | null;\n while ((m = openRe.exec(html)) !== null) {\n if (m.index >= start) break;\n if (!best || m.index > best.idx) {\n best = {idx: m.index, contentStart: m.index + m[0].length, tag};\n }\n }\n }\n if (!best) return null;\n const closeRe = new RegExp(`</${best.tag}\\\\s*>`, 'gi');\n closeRe.lastIndex = end;\n const close = closeRe.exec(html);\n if (!close) return null;\n return {\n block: {start: best.idx, end: close.index + close[0].length},\n contentStart: best.contentStart,\n contentEnd: close.index,\n };\n}\n\n/**\n * What to delete for a marker: its whole paragraph when the marker is alone in\n * it (removing only the text would leave a blank line), else just the marker.\n */\nfunction markerSpan(html: string, start: number, end: number): Span {\n const found = enclosingBlock(html, start, end);\n if (!found || found.contentStart > start || found.contentEnd < end) return {start, end};\n const rest = html.slice(found.contentStart, start) + html.slice(end, found.contentEnd);\n return stripToText(rest).trim() === '' ? found.block : {start, end};\n}\n\nfunction locate(html: string, token: string): Span | null {\n const m = markerRegex(token).exec(html);\n return m ? {start: m.index, end: m.index + m[0].length} : null;\n}\n\n// ── Single-token fill ───────────────────────────────────────────────────\n//\n// The section machinery above is for templates holding several variants of\n// one email. These are for the simpler case: fixed wording with details typed\n// elsewhere and dropped into tokens of the caller's own choosing (`{Name}`,\n// `%NAME%`, …). Same tag-tolerant matching, since these templates are edited in\n// Outlook too.\n\n/** Replace every occurrence of `token` with `value`, which is never rescanned. */\nexport function replaceToken(html: string, token: string, value: string): string {\n let done = '';\n let rest = html;\n for (; ;) {\n const at = locate(rest, token);\n if (!at) return done + rest;\n done += rest.slice(0, at.start) + value;\n rest = rest.slice(at.end);\n }\n}\n\n/**\n * Remove `token` together with the line it sits on — the table row or\n * paragraph, label included. For an optional detail this is the difference\n * between the email not mentioning it and a heading with nothing under it.\n * Falls back to deleting just the token when it isn't inside a line element.\n */\nexport function removeTokenLine(html: string, token: string): string {\n let out = html;\n for (; ;) {\n const at = locate(out, token);\n if (!at) return out;\n const cut = lineSpan(out, at);\n out = out.slice(0, cut.start) + out.slice(cut.end);\n }\n}\n\n/**\n * The line to delete for a token at `at`: its enclosing row or paragraph, or\n * the token's own span when it isn't in one.\n *\n * `enclosingBlock` returns the *nearest* opening tag before the token, which is\n * only the enclosing one if it hasn't been closed already — a token in a\n * paragraph after a table has a `<tr>` before it and a `</tr>` after it without\n * being inside a row, and taking that span would delete the table.\n */\nfunction lineSpan(html: string, at: Span): Span {\n for (const tag of LINE_TAGS) {\n const found = enclosingBlock(html, at.start, at.end, [tag]);\n if (!found) continue;\n const closedBefore = new RegExp(`</${tag}\\\\s*>`, 'i').test(html.slice(found.contentStart, at.start));\n if (!closedBefore) return found.block;\n }\n return at;\n}\n\n/** Which of `tokens` the template actually uses — what the caller has to supply. */\nexport function findTokens(html: string, tokens: readonly string[]): string[] {\n const text = stripToText(html || '');\n return tokens.filter(token => markerRegex(token).test(text));\n}\n\n/**\n * `{Token}`-shaped text still in a filled-in body: a placeholder misspelled\n * while editing, or one nothing supplied. Reads visible text only, so the\n * braces of a <style> block are not mistaken for placeholders. Catches a\n * leftover `{{TOKEN}}` too, reported by its inner `{TOKEN}`.\n */\nexport function findUnfilledTokens(html: string): string[] {\n const text = stripToText((html || '').replace(/<(style|script)\\b[^>]*>[\\s\\S]*?<\\/\\1>/gi, ''));\n const found: string[] = [];\n const re = /\\{\\s*([A-Za-z][A-Za-z0-9 _-]{0,30})\\s*\\}/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n const token = `{${m[1].trim()}}`;\n if (!found.includes(token)) found.push(token);\n }\n return found;\n}\n\nexport interface ComposeOptions {\n /** Section to keep; the other sections are dropped with their markers. */\n section?: string;\n /** `{{NAME}}` → HTML to substitute. Names match case-insensitively. */\n placeholders?: Readonly<Record<string, string>>;\n /** The template's name, for error messages. */\n label?: string;\n}\n\n/**\n * Reduce a template body to the one email that goes out: keep `section`, drop\n * the rest, fill the placeholders. Throws INVALID_REQUEST rather than guessing —\n * a template edited into an unusable state has to stop the send, not deliver a\n * body with `[[ACCEPT]]` or an unfilled placeholder in it.\n */\nexport function composeTemplateBody(html: string, options: ComposeOptions = {}): string {\n const label = options.label ? `Template '${options.label}'` : 'The template';\n const wanted = (options.section || '').trim().toUpperCase();\n const {sections, placeholders: available} = findTemplateMarkers(html);\n\n if (wanted && !sections.includes(wanted)) {\n throw new InvalidRequestError(\n sections.length\n ? `${label} has no [[${wanted}]] section. Sections found: ${sections.join(', ')}.`\n : `${label} has no [[${wanted}]]...[[/${wanted}]] markers — it holds a single body, so name no section (or add the markers to the template).`,\n );\n }\n if (!wanted && sections.length) {\n throw new InvalidRequestError(\n `${label} is split into sections (${sections.join(', ')}); name the one to send, `\n + 'otherwise the email would go out with every variant and the markers in it.',\n );\n }\n\n // Deletions are collected first and applied right to left, so each span's\n // offsets still refer to the string they were measured against.\n const cuts: Span[] = [];\n for (const name of sections) {\n const open = locate(html, `[[${name}]]`);\n const close = locate(html, `[[/${name}]]`);\n if (!open || !close || close.start < open.end) {\n throw new InvalidRequestError(`${label} has a malformed [[${name}]] section — expected [[${name}]] ... [[/${name}]] in that order.`);\n }\n const openSpan = markerSpan(html, open.start, open.end);\n const closeSpan = markerSpan(html, close.start, close.end);\n if (name === wanted) {\n cuts.push(openSpan, closeSpan); // keep the content, shed the markers\n } else {\n cuts.push({start: openSpan.start, end: closeSpan.end});\n }\n }\n let out = html;\n for (const cut of cuts.sort((a, b) => b.start - a.start)) {\n out = out.slice(0, cut.start) + out.slice(cut.end);\n }\n\n for (const [rawKey, value] of Object.entries(options.placeholders || {})) {\n const key = rawKey.trim().toUpperCase();\n if (!available.includes(key)) {\n throw new InvalidRequestError(\n `${label} has no {{${key}}} placeholder`\n + (available.length ? ` (found: ${available.map(p => `{{${p}}}`).join(', ')}).` : '.')\n + ' It may have been removed when the template was last edited.',\n );\n }\n const m = markerRegex(`{{${key}}}`).exec(out);\n // In the template but gone from `out` means it lived in a dropped\n // section: the caller supplying more than one variant needs, not an error.\n if (m) out = out.slice(0, m.index) + (value ?? '') + out.slice(m.index + m[0].length);\n }\n\n const leftover = markerTokens(out);\n if (leftover.length) {\n const hint = leftover.every(t => t.startsWith('{{'))\n ? 'Supply a value for each placeholder.'\n : 'Check the template — every [[SECTION]] needs its [[/SECTION]].';\n throw new InvalidRequestError(\n `${label} still contains unresolved markers after composing: ${leftover.join(', ')}. ${hint} Nothing was created.`,\n );\n }\n return out;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"templateBody.js","sourceRoot":"","sources":["../src/templateBody.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;;;AAqEH,kDAuBC;AA0FD,oCASC;AAQD,0CAQC;AAsBD,gCAGC;AAQD,gDAUC;AAiBD,kDAkEC;AA3UD,qCAA+C;AAE/C,kFAAkF;AAClF,MAAM,IAAI,GAAG,qBAAqB,CAAC;AAEnC;;;;;GAKG;AACH,MAAM,GAAG,GAAG,kDAAkD,CAAC;AAE/D,uFAAuF;AACvF,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;AAEtC;;;;;;;;;;GAUG;AACH,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;AAEpC,SAAS,QAAQ,CAAC,CAAS;IACvB,OAAO,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAkB,CAAC;AAEnD,SAAS,WAAW,CAAC,KAAa;IAC9B,IAAI,KAAK,GAAG,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK,EAAE,CAAC;QACT,KAAK,GAAG,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;QACjE,gBAAgB,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACvC,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,sFAAsF;AACtF,SAAS,WAAW,CAAC,IAAY;IAC7B,OAAO,IAAI;SACN,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;SAC/B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC;AACzC,CAAC;AASD,6FAA6F;AAC7F,SAAgB,mBAAmB,CAAC,IAAY;IAC5C,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IACrC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IACjC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,SAAS,GAAG,IAAI,MAAM,CAAC,sBAAsB,IAAI,aAAa,EAAE,GAAG,CAAC,CAAC;IAC3E,IAAI,CAAyB,CAAC;IAC9B,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACzC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QAChC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACP,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;aAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACf,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC;IACL,CAAC;IACD,MAAM,YAAY,GAAa,EAAE,CAAC;IAClC,MAAM,aAAa,GAAG,IAAI,MAAM,CAAC,cAAc,IAAI,aAAa,EAAE,GAAG,CAAC,CAAC;IACvE,OAAO,CAAC,CAAC,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC7C,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QAChC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC;AACzE,CAAC;AAED;;;;GAIG;AACU,QAAA,mBAAmB,GAAG,mBAAmB,IAAI,wBAAwB,IAAI,YAAY,CAAC;AAEnG;;;;GAIG;AACH,SAAS,YAAY,CAAC,IAAY;IAC9B,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IACrC,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,2BAAmB,EAAE,GAAG,CAAC,CAAC;IAChD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,CAAyB,CAAC;IAC9B,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACvC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC;AAOD;;;;;GAKG;AACH,SAAS,cAAc,CAAC,IAAY,EAAE,KAAa,EAAE,GAAW,EAAE,OAAiB,UAAU;IAKzF,IAAI,IAAI,GAA8D,IAAI,CAAC;IAC3E,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,GAAG,gBAAgB,EAAE,IAAI,CAAC,CAAC;QACzD,IAAI,CAAyB,CAAC;QAC9B,OAAO,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YACtC,IAAI,CAAC,CAAC,KAAK,IAAI,KAAK;gBAAE,MAAM;YAC5B,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC9B,IAAI,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,KAAK,EAAE,YAAY,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC;YACtE,CAAC;QACL,CAAC;IACL,CAAC;IACD,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,OAAO,GAAG,IAAI,MAAM,CAAC,KAAK,IAAI,CAAC,GAAG,OAAO,EAAE,IAAI,CAAC,CAAC;IACvD,OAAO,CAAC,SAAS,GAAG,GAAG,CAAC;IACxB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,OAAO;QACH,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE;QAC9D,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,UAAU,EAAE,KAAK,CAAC,KAAK;KAC1B,CAAC;AACN,CAAC;AAED;;;GAGG;AACH,SAAS,UAAU,CAAC,IAAY,EAAE,KAAa,EAAE,GAAW;IACxD,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;IAC/C,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,YAAY,GAAG,KAAK,IAAI,KAAK,CAAC,UAAU,GAAG,GAAG;QAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;IAC1F,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IACvF,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;AAC1E,CAAC;AAED,SAAS,MAAM,CAAC,IAAY,EAAE,KAAa;IACvC,MAAM,CAAC,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACrE,CAAC;AAED,2EAA2E;AAC3E,EAAE;AACF,2EAA2E;AAC3E,8EAA8E;AAC9E,4EAA4E;AAC5E,gFAAgF;AAChF,eAAe;AAEf,kFAAkF;AAClF,SAAgB,YAAY,CAAC,IAAY,EAAE,KAAa,EAAE,KAAa;IACnE,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,IAAI,GAAG,IAAI,CAAC;IAChB,SAAU,CAAC;QACP,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC/B,IAAI,CAAC,EAAE;YAAE,OAAO,IAAI,GAAG,IAAI,CAAC;QAC5B,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC;QACxC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IAC9B,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,SAAgB,eAAe,CAAC,IAAY,EAAE,KAAa;IACvD,IAAI,GAAG,GAAG,IAAI,CAAC;IACf,SAAU,CAAC;QACP,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC9B,IAAI,CAAC,EAAE;YAAE,OAAO,GAAG,CAAC;QACpB,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAC9B,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACvD,CAAC;AACL,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,QAAQ,CAAC,IAAY,EAAE,EAAQ;IACpC,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAC1B,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAC5D,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,MAAM,YAAY,GAAG,IAAI,MAAM,CAAC,KAAK,GAAG,OAAO,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,YAAY,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;QACrG,IAAI,CAAC,YAAY;YAAE,OAAO,KAAK,CAAC,KAAK,CAAC;IAC1C,CAAC;IACD,OAAO,EAAE,CAAC;AACd,CAAC;AAED,oFAAoF;AACpF,SAAgB,UAAU,CAAC,IAAY,EAAE,MAAyB;IAC9D,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IACrC,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AACjE,CAAC;AAED;;;;;GAKG;AACH,SAAgB,kBAAkB,CAAC,IAAY;IAC3C,MAAM,IAAI,GAAG,WAAW,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,yCAAyC,EAAE,EAAE,CAAC,CAAC,CAAC;IAC9F,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,EAAE,GAAG,2CAA2C,CAAC;IACvD,IAAI,CAAyB,CAAC;IAC9B,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC;QACjC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC;AAWD;;;;;GAKG;AACH,SAAgB,mBAAmB,CAAC,IAAY,EAAE,UAA0B,EAAE;IAC1E,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,aAAa,OAAO,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC;IAC7E,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC5D,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,SAAS,EAAE,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAExE,IAAI,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,4BAAmB,CACzB,QAAQ,CAAC,MAAM;YACX,CAAC,CAAC,GAAG,KAAK,aAAa,MAAM,+BAA+B,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;YAClF,CAAC,CAAC,GAAG,KAAK,aAAa,MAAM,WAAW,MAAM,+FAA+F,CACpJ,CAAC;IACN,CAAC;IACD,IAAI,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;QAC7B,MAAM,IAAI,4BAAmB,CACzB,GAAG,KAAK,4BAA4B,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,2BAA2B;cAChF,4EAA4E,CACjF,CAAC;IACN,CAAC;IAED,0EAA0E;IAC1E,gEAAgE;IAChE,MAAM,IAAI,GAAW,EAAE,CAAC;IACxB,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,KAAK,IAAI,IAAI,CAAC,CAAC;QACzC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,MAAM,IAAI,IAAI,CAAC,CAAC;QAC3C,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAC5C,MAAM,IAAI,4BAAmB,CAAC,GAAG,KAAK,sBAAsB,IAAI,2BAA2B,IAAI,aAAa,IAAI,mBAAmB,CAAC,CAAC;QACzI,CAAC;QACD,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QACxD,MAAM,SAAS,GAAG,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAC3D,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YAClB,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,qCAAqC;QACzE,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,GAAG,EAAE,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC;QAC7D,CAAC;IACL,CAAC;IACD,IAAI,GAAG,GAAG,IAAI,CAAC;IACf,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;QACvD,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACvD,CAAC;IAED,KAAK,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC,EAAE,CAAC;QACvE,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACxC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,4BAAmB,CACzB,GAAG,KAAK,aAAa,GAAG,gBAAgB;kBACtC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC;kBACpF,8DAA8D,CACnE,CAAC;QACN,CAAC;QACD,MAAM,CAAC,GAAG,WAAW,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC9C,kEAAkE;QAClE,2EAA2E;QAC3E,IAAI,CAAC;YAAE,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAC1F,CAAC;IAED,MAAM,QAAQ,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;QAClB,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YAChD,CAAC,CAAC,sCAAsC;YACxC,CAAC,CAAC,gEAAgE,CAAC;QACvE,MAAM,IAAI,4BAAmB,CACzB,GAAG,KAAK,uDAAuD,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,uBAAuB,CACrH,CAAC;IACN,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC","sourcesContent":["/**\n * Sections and placeholders in HTML email templates.\n *\n * A template email is an ordinary saved mail its owner edits in Outlook, so the\n * greeting, closing and signature stay theirs. Where one template serves several\n * kinds of reply, the variants live inside it between markers:\n *\n * Hello,\n * [[ACCEPT]] Thank you — we accept... [[/ACCEPT]]\n * [[DECLINE]] Thank you for letting us know... [[/DECLINE]]\n * [[CLARIFY]] {{QUESTIONS}} [[/CLARIFY]]\n * Best regards, {{SIGNATURE}}\n *\n * Composing keeps the requested section, drops the others, and substitutes the\n * `{{PLACEHOLDER}}`s — so shared wording is written once and a caller names a\n * section instead of carrying the whole body.\n *\n * Everything works on the raw HTML string rather than a parsed tree: Word's\n * markup carries conditional comments and namespaced tags a parser round-trip\n * would rewrite, and the composed body must stay byte-identical to what was\n * written.\n */\n\nimport { InvalidRequestError } from './errors';\n\n/** Marker and placeholder names: what a person can reasonably type in Outlook. */\nconst NAME = '[A-Za-z0-9_-]{1,40}';\n\n/**\n * What Word may inject *between two characters of a marker*: tags (it splits\n * runs across <span>s freely), comments, non-breaking spaces, and the newlines\n * it wraps long lines with — sometimes mid-word. So `[[ACCEPT]]` is matched\n * character by character with this between, never as a literal string.\n */\nconst GAP = '(?:<!--[\\\\s\\\\S]*?-->|<[^>]*>| | |\\\\s)*';\n\n/** Block elements a marker can sit alone inside; the whole block goes when it does. */\nconst BLOCK_TAGS = ['p', 'div', 'li'];\n\n/**\n * Block elements that make up one *line* of a template, for a token whose\n * empty value takes its whole line out (see `removeTokenLine`). Outermost\n * first: Word puts a <p> inside every table cell, so a token in a details table\n * has to drop the <tr> — dropping the <p> would leave the row's label beside a\n * blank cell.\n *\n * <div> is deliberately absent: it is rarely a line and often the wrapper around\n * the entire body, so a token directly inside one would take the whole email\n * with it. Deleting just the token is the safer miss.\n */\nconst LINE_TAGS = ['tr', 'li', 'p'];\n\nfunction escapeRe(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * Tag-tolerant matcher for one literal token, e.g. `[[/ACCEPT]]` or `{{QUESTIONS}}`.\n *\n * Memoized: the pattern interleaves `GAP` between every character, so a short\n * token compiles to hundreds of characters, and the replace loops ask for the\n * same token repeatedly while walking a template.\n */\nconst markerRegexCache = new Map<string, RegExp>();\n\nfunction markerRegex(token: string): RegExp {\n let regex = markerRegexCache.get(token);\n if (!regex) {\n regex = new RegExp(token.split('').map(escapeRe).join(GAP), 'i');\n markerRegexCache.set(token, regex);\n }\n return regex;\n}\n\n/** HTML → rough plain text, enough to spot markers and test a block for emptiness. */\nfunction stripToText(html: string): string {\n return html\n .replace(/<!--[\\s\\S]*?-->/g, '')\n .replace(/<[^>]*>/g, '')\n .replace(/ | /gi, ' ');\n}\n\nexport interface TemplateMarkers {\n /** Section names with both an opening and a closing marker, in document order. */\n sections: string[];\n /** `{{NAME}}` placeholders found anywhere in the template. */\n placeholders: string[];\n}\n\n/** Find the markers in a template body (HTML, or the plain text Outlook derives from it). */\nexport function findTemplateMarkers(body: string): TemplateMarkers {\n const text = stripToText(body || '');\n const opened: string[] = [];\n const closed = new Set<string>();\n const seen = new Set<string>();\n const sectionRe = new RegExp(`\\\\[\\\\[\\\\s*(/?)\\\\s*(${NAME})\\\\s*\\\\]\\\\]`, 'g');\n let m: RegExpExecArray | null;\n while ((m = sectionRe.exec(text)) !== null) {\n const name = m[2].toUpperCase();\n if (m[1]) {\n closed.add(name);\n } else if (!seen.has(name)) {\n seen.add(name);\n opened.push(name);\n }\n }\n const placeholders: string[] = [];\n const placeholderRe = new RegExp(`\\\\{\\\\{\\\\s*(${NAME})\\\\s*\\\\}\\\\}`, 'g');\n while ((m = placeholderRe.exec(text)) !== null) {\n const name = m[1].toUpperCase();\n if (!placeholders.includes(name)) placeholders.push(name);\n }\n return { sections: opened.filter(n => closed.has(n)), placeholders };\n}\n\n/**\n * Any `[[SECTION]]`, `[[/SECTION]]` or `{{PLACEHOLDER}}` token, paired or not, as\n * a pattern source JavaScript and .NET read the same way: Windows scans a\n * template's plain text with it inside PowerShell.\n */\nexport const MARKER_TOKEN_SOURCE = `\\\\[\\\\[\\\\s*/?\\\\s*${NAME}\\\\s*\\\\]\\\\]|\\\\{\\\\{\\\\s*${NAME}\\\\s*\\\\}\\\\}`;\n\n/**\n * Every `[[...]]` / `{{...}}` token still in a body, paired or not — the last\n * check before a composed body goes out, so a half-deleted marker stops the\n * send instead of reaching a recipient.\n */\nfunction markerTokens(body: string): string[] {\n const text = stripToText(body || '');\n const re = new RegExp(MARKER_TOKEN_SOURCE, 'g');\n const found: string[] = [];\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n const token = m[0].replace(/\\s+/g, '');\n if (!found.includes(token)) found.push(token);\n }\n return found;\n}\n\ninterface Span {\n start: number;\n end: number;\n}\n\n/**\n * The nearest enclosing block element of [start, end), or null when the marker\n * isn't inside one. The closing tag is the first of the same name after the\n * marker — Word doesn't nest <p>, and where that guess is wrong the emptiness\n * test simply fails and the caller falls back to the marker's own span.\n */\nfunction enclosingBlock(html: string, start: number, end: number, tags: string[] = BLOCK_TAGS): {\n block: Span;\n contentStart: number;\n contentEnd: number;\n} | null {\n let best: { idx: number; contentStart: number; tag: string } | null = null;\n for (const tag of tags) {\n const openRe = new RegExp(`<${tag}(?:\\\\s[^>]*)?>`, 'gi');\n let m: RegExpExecArray | null;\n while ((m = openRe.exec(html)) !== null) {\n if (m.index >= start) break;\n if (!best || m.index > best.idx) {\n best = { idx: m.index, contentStart: m.index + m[0].length, tag };\n }\n }\n }\n if (!best) return null;\n const closeRe = new RegExp(`</${best.tag}\\\\s*>`, 'gi');\n closeRe.lastIndex = end;\n const close = closeRe.exec(html);\n if (!close) return null;\n return {\n block: { start: best.idx, end: close.index + close[0].length },\n contentStart: best.contentStart,\n contentEnd: close.index,\n };\n}\n\n/**\n * What to delete for a marker: its whole paragraph when the marker is alone in\n * it (removing only the text would leave a blank line), else just the marker.\n */\nfunction markerSpan(html: string, start: number, end: number): Span {\n const found = enclosingBlock(html, start, end);\n if (!found || found.contentStart > start || found.contentEnd < end) return { start, end };\n const rest = html.slice(found.contentStart, start) + html.slice(end, found.contentEnd);\n return stripToText(rest).trim() === '' ? found.block : { start, end };\n}\n\nfunction locate(html: string, token: string): Span | null {\n const m = markerRegex(token).exec(html);\n return m ? { start: m.index, end: m.index + m[0].length } : null;\n}\n\n// ── Single-token fill ───────────────────────────────────────────────────\n//\n// The section machinery above is for templates holding several variants of\n// one email. These are for the simpler case: fixed wording with details typed\n// elsewhere and dropped into tokens of the caller's own choosing (`{Name}`,\n// `%NAME%`, …). Same tag-tolerant matching, since these templates are edited in\n// Outlook too.\n\n/** Replace every occurrence of `token` with `value`, which is never rescanned. */\nexport function replaceToken(html: string, token: string, value: string): string {\n let done = '';\n let rest = html;\n for (; ;) {\n const at = locate(rest, token);\n if (!at) return done + rest;\n done += rest.slice(0, at.start) + value;\n rest = rest.slice(at.end);\n }\n}\n\n/**\n * Remove `token` together with the line it sits on — the table row or\n * paragraph, label included. For an optional detail this is the difference\n * between the email not mentioning it and a heading with nothing under it.\n * Falls back to deleting just the token when it isn't inside a line element.\n */\nexport function removeTokenLine(html: string, token: string): string {\n let out = html;\n for (; ;) {\n const at = locate(out, token);\n if (!at) return out;\n const cut = lineSpan(out, at);\n out = out.slice(0, cut.start) + out.slice(cut.end);\n }\n}\n\n/**\n * The line to delete for a token at `at`: its enclosing row or paragraph, or\n * the token's own span when it isn't in one.\n *\n * `enclosingBlock` returns the *nearest* opening tag before the token, which is\n * only the enclosing one if it hasn't been closed already — a token in a\n * paragraph after a table has a `<tr>` before it and a `</tr>` after it without\n * being inside a row, and taking that span would delete the table.\n */\nfunction lineSpan(html: string, at: Span): Span {\n for (const tag of LINE_TAGS) {\n const found = enclosingBlock(html, at.start, at.end, [tag]);\n if (!found) continue;\n const closedBefore = new RegExp(`</${tag}\\\\s*>`, 'i').test(html.slice(found.contentStart, at.start));\n if (!closedBefore) return found.block;\n }\n return at;\n}\n\n/** Which of `tokens` the template actually uses — what the caller has to supply. */\nexport function findTokens(html: string, tokens: readonly string[]): string[] {\n const text = stripToText(html || '');\n return tokens.filter(token => markerRegex(token).test(text));\n}\n\n/**\n * `{Token}`-shaped text still in a filled-in body: a placeholder misspelled\n * while editing, or one nothing supplied. Reads visible text only, so the\n * braces of a <style> block are not mistaken for placeholders. Catches a\n * leftover `{{TOKEN}}` too, reported by its inner `{TOKEN}`.\n */\nexport function findUnfilledTokens(html: string): string[] {\n const text = stripToText((html || '').replace(/<(style|script)\\b[^>]*>[\\s\\S]*?<\\/\\1>/gi, ''));\n const found: string[] = [];\n const re = /\\{\\s*([A-Za-z][A-Za-z0-9 _-]{0,30})\\s*\\}/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n const token = `{${m[1].trim()}}`;\n if (!found.includes(token)) found.push(token);\n }\n return found;\n}\n\nexport interface ComposeOptions {\n /** Section to keep; the other sections are dropped with their markers. */\n section?: string;\n /** `{{NAME}}` → HTML to substitute. Names match case-insensitively. */\n placeholders?: Readonly<Record<string, string>>;\n /** The template's name, for error messages. */\n label?: string;\n}\n\n/**\n * Reduce a template body to the one email that goes out: keep `section`, drop\n * the rest, fill the placeholders. Throws INVALID_REQUEST rather than guessing —\n * a template edited into an unusable state has to stop the send, not deliver a\n * body with `[[ACCEPT]]` or an unfilled placeholder in it.\n */\nexport function composeTemplateBody(html: string, options: ComposeOptions = {}): string {\n const label = options.label ? `Template '${options.label}'` : 'The template';\n const wanted = (options.section || '').trim().toUpperCase();\n const { sections, placeholders: available } = findTemplateMarkers(html);\n\n if (wanted && !sections.includes(wanted)) {\n throw new InvalidRequestError(\n sections.length\n ? `${label} has no [[${wanted}]] section. Sections found: ${sections.join(', ')}.`\n : `${label} has no [[${wanted}]]...[[/${wanted}]] markers — it holds a single body, so name no section (or add the markers to the template).`,\n );\n }\n if (!wanted && sections.length) {\n throw new InvalidRequestError(\n `${label} is split into sections (${sections.join(', ')}); name the one to send, `\n + 'otherwise the email would go out with every variant and the markers in it.',\n );\n }\n\n // Deletions are collected first and applied right to left, so each span's\n // offsets still refer to the string they were measured against.\n const cuts: Span[] = [];\n for (const name of sections) {\n const open = locate(html, `[[${name}]]`);\n const close = locate(html, `[[/${name}]]`);\n if (!open || !close || close.start < open.end) {\n throw new InvalidRequestError(`${label} has a malformed [[${name}]] section — expected [[${name}]] ... [[/${name}]] in that order.`);\n }\n const openSpan = markerSpan(html, open.start, open.end);\n const closeSpan = markerSpan(html, close.start, close.end);\n if (name === wanted) {\n cuts.push(openSpan, closeSpan); // keep the content, shed the markers\n } else {\n cuts.push({ start: openSpan.start, end: closeSpan.end });\n }\n }\n let out = html;\n for (const cut of cuts.sort((a, b) => b.start - a.start)) {\n out = out.slice(0, cut.start) + out.slice(cut.end);\n }\n\n for (const [rawKey, value] of Object.entries(options.placeholders || {})) {\n const key = rawKey.trim().toUpperCase();\n if (!available.includes(key)) {\n throw new InvalidRequestError(\n `${label} has no {{${key}}} placeholder`\n + (available.length ? ` (found: ${available.map(p => `{{${p}}}`).join(', ')}).` : '.')\n + ' It may have been removed when the template was last edited.',\n );\n }\n const m = markerRegex(`{{${key}}}`).exec(out);\n // In the template but gone from `out` means it lived in a dropped\n // section: the caller supplying more than one variant needs, not an error.\n if (m) out = out.slice(0, m.index) + (value ?? '') + out.slice(m.index + m[0].length);\n }\n\n const leftover = markerTokens(out);\n if (leftover.length) {\n const hint = leftover.every(t => t.startsWith('{{'))\n ? 'Supply a value for each placeholder.'\n : 'Check the template — every [[SECTION]] needs its [[/SECTION]].';\n throw new InvalidRequestError(\n `${label} still contains unresolved markers after composing: ${leftover.join(', ')}. ${hint} Nothing was created.`,\n );\n }\n return out;\n}\n"]}
|
package/dist/types.d.ts
CHANGED
|
@@ -49,6 +49,18 @@ export interface SendEmailParams {
|
|
|
49
49
|
*/
|
|
50
50
|
openDraftWindow?: boolean;
|
|
51
51
|
}
|
|
52
|
+
export interface SendEmailResult {
|
|
53
|
+
/**
|
|
54
|
+
* The draft's entry id when the email was filed silently into Drafts
|
|
55
|
+
* (`openDraftWindow: false`) — the id `listOutlookDrafts` reports for it,
|
|
56
|
+
* so a caller can keep it and later send or delete exactly the drafts it
|
|
57
|
+
* made. '' when the email was sent, or opened in a compose window, which
|
|
58
|
+
* files nothing yet; and on macOS in the rare case Drafts gained another
|
|
59
|
+
* draft with the same subject in the same moment, so this one could not be
|
|
60
|
+
* told apart.
|
|
61
|
+
*/
|
|
62
|
+
draftEntryId: string;
|
|
63
|
+
}
|
|
52
64
|
export interface ReplyEmailParams extends EmailLocator {
|
|
53
65
|
/** SMTP address of the mailbox to reply from. */
|
|
54
66
|
emailAccount: string;
|
|
@@ -296,8 +308,9 @@ export interface InboxSearchFilter {
|
|
|
296
308
|
subjectLike?: string;
|
|
297
309
|
/**
|
|
298
310
|
* Regular expression tested against each subject, case-insensitively
|
|
299
|
-
* whatever the RegExp's own flags
|
|
300
|
-
*
|
|
311
|
+
* whatever the RegExp's own flags; on macOS a `u` or `v` flag is kept,
|
|
312
|
+
* since it decides how the pattern parses. On Windows the pattern runs in
|
|
313
|
+
* .NET's regex engine, so keep to syntax JavaScript and .NET share.
|
|
301
314
|
*/
|
|
302
315
|
subjectPattern?: RegExp;
|
|
303
316
|
/** Drop subjects carrying a reply or forward prefix (RE:, FW:, AW:, 回复:, …). */
|
|
@@ -376,6 +389,13 @@ export interface SelectedEmail extends EmailLocator {
|
|
|
376
389
|
body: string;
|
|
377
390
|
/** Not saved to disk — pass the ones you want to saveEmailAttachments. */
|
|
378
391
|
attachmentNames: string[];
|
|
392
|
+
/**
|
|
393
|
+
* The folder it sits in, in the form the listings report
|
|
394
|
+
* (`\\team@example.com\[Gmail]\Drafts`) and so naming its mailbox. '' when
|
|
395
|
+
* Outlook can't say: an email opened from a file, or on macOS one whose
|
|
396
|
+
* mailbox neither AppleScript nor the profile database names.
|
|
397
|
+
*/
|
|
398
|
+
folderPath: string;
|
|
379
399
|
}
|
|
380
400
|
export interface ReadEmailBodyOptions {
|
|
381
401
|
/** Cap on the returned body. Default 8000. */
|
|
@@ -507,7 +527,7 @@ export interface OutlookBridge {
|
|
|
507
527
|
/** Every mailbox this Outlook profile can reach, as SMTP addresses. */
|
|
508
528
|
getOutlookAccounts(): Promise<string[]>;
|
|
509
529
|
/** Send an email, or stage it as a draft. */
|
|
510
|
-
sendOutlookEmail(params: SendEmailParams): Promise<
|
|
530
|
+
sendOutlookEmail(params: SendEmailParams): Promise<SendEmailResult>;
|
|
511
531
|
/** Reply to an email, with the new text above the quoted original. */
|
|
512
532
|
replyOutlookEmail(params: ReplyEmailParams): Promise<ReplyEmailResult>;
|
|
513
533
|
/** Recent mail from the Inbox root or one named folder, newest first. */
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA,wEAAwE;AACxE,wDAAwD;AACxD,EAAE;AACF,mCAAmC;AACnC,+EAA+E;AAC/E,8EAA8E;AAC9E,YAAY;AACZ,iFAAiF;AACjF,+BAA+B","sourcesContent":["// The public contract: what the package returns, what it takes, and the\n// OutlookBridge interface every entry point answers to.\n//\n// Two conventions hold throughout:\n// - Timestamps are local time as 'yyyy-MM-dd HH:mm', which sorts correctly as\n// a plain string and is what both platforms can produce without a timezone\n// table.\n// - Ids are platform-specific (see EmailLocator). Don't persist one and look it\n// up on the other platform.\n\n// ── Identifying an email ─────────────────────────────────────────────────\n/**\n * Where an email lives, as a listing reported it.\n *\n * Every listing row (`InboxEmail`, `InboxSearchMatch`, `SelectedEmail`, …)\n * carries both fields, so a row can be passed straight back in wherever an\n * `EmailRef` is accepted — which is also the reliable way to keep the two ids\n * from different rows apart.\n */\nexport interface EmailLocator {\n /** Windows: the MAPI EntryID. macOS: Outlook for Mac's integer message id. */\n readonly entryId: string;\n /**\n * Windows: the StoreID the email was listed from, which lets it resolve in\n * any mounted mailbox rather than only the default one. macOS has no\n * equivalent and ignores it.\n */\n readonly storeId?: string;\n}\n\n/** An email to act on: its entry id alone, or anything carrying a locator. */\nexport type EmailRef = string | EmailLocator;\n\n/**\n * Recipients: one address, a comma- or semicolon-separated list, or an array.\n * Display-name forms such as `\"Doe, Jo\" <jo@example.com>` are kept whole.\n */\nexport type Recipients = string | readonly string[];\n\n// ── Sending and replying ─────────────────────────────────────────────────\nexport interface SendEmailParams {\n /** SMTP address of the mailbox to send from. Never falls back to the\n * default account — an address that doesn't resolve is an error. */\n emailAccount: string;\n to?: Recipients;\n cc?: Recipients;\n bcc?: Recipients;\n subject: string;\n htmlBody: string;\n /** Absolute paths of files to attach. Each has to exist. */\n attachments?: readonly string[];\n /**\n * An Outlook signature, by the name listOutlookSignatures reports, added to\n * the body: in its `{{SIGNATURE}}` placeholder when it has one, otherwise\n * at the end. Outlook adds no signature of its own to a body set this way,\n * so this is how a composed email gets one.\n */\n signatureName?: string;\n /** Send now. Default false: the email is staged as a draft instead. */\n sendImmediately?: boolean;\n /**\n * Drafts only. True (the default) opens an Outlook compose window; false\n * files the draft silently into Drafts — the workable choice for batches.\n */\n openDraftWindow?: boolean;\n}\n\nexport interface ReplyEmailParams extends EmailLocator {\n /** SMTP address of the mailbox to reply from. */\n emailAccount: string;\n /**\n * HTML inserted above the quoted original (a full document is reduced to\n * its <body> content). Optional when `templateSubject` is given.\n */\n htmlBody?: string;\n /**\n * Reply with a template email saved in the mailbox, found by subject, so\n * the caller never carries a large template's HTML itself.\n */\n templateSubject?: string;\n /** Folder holding the template. Default 'Templates'. */\n templateFolder?: string;\n /**\n * The [[SECTION]] of the template to keep, when one template holds several\n * reply variants (see composeTemplateBody). Required if it has sections.\n */\n templateSection?: string;\n /** `{{PLACEHOLDER}}` → HTML substituted into the composed body. */\n templatePlaceholders?: Readonly<Record<string, string>>;\n /**\n * An Outlook signature (by the name listOutlookSignatures reports), put in\n * the body's `{{SIGNATURE}}` placeholder, or below the new text when there\n * is none. Resolved here, so the signature's HTML never crosses the\n * caller's boundary.\n */\n signatureName?: string;\n /**\n * Answer every recipient of the original instead of only its sender.\n * Default false: widening a reply cannot be taken back, so it is opt-in.\n */\n replyAll?: boolean;\n /** Send now. Default false. */\n sendImmediately?: boolean;\n /** Drafts only: open a compose window (the default) or file silently. */\n openDraftWindow?: boolean;\n}\n\nexport interface ReplyEmailResult {\n /** Who the reply is addressed to. Windows reports Outlook's To line\n * (display names); macOS the resolved addresses. */\n to: string;\n subject: string;\n /** The original email's sender — check it is who you meant to answer. */\n repliedToSender: string;\n}\n\n// ── Per-item outcomes ────────────────────────────────────────────────────\n/** One item a batch operation could not handle, and why. */\nexport interface ItemFailure {\n entryId: string;\n /** The item's subject when it could be read, else ''. */\n subject: string;\n error: string;\n}\n\n// ── Drafts ───────────────────────────────────────────────────────────────\n/** One mail draft belonging to an account. */\nexport interface OutlookDraft {\n entryId: string;\n subject: string;\n /** The To line as Outlook renders it. */\n to: string;\n /** Recipient addresses, best effort — an unresolved Exchange entry falls back to its name. */\n toEmails: string[];\n /** Short plain-text preview, enough to tell similar drafts apart. */\n bodyPreview: string;\n hasAttachments: boolean;\n lastModified: string;\n /** The Drafts folder it sits in; one account's drafts can span two. */\n folderPath: string;\n}\n\nexport interface ListDraftsOptions {\n /** Newest-first cap on the drafts returned. Default 100. */\n limit?: number;\n /** Characters of body preview per draft. Default 300; 0 omits it. */\n previewChars?: number;\n}\n\nexport interface ListDraftsResult {\n account: string;\n /** The Drafts folders scanned, in scan order. */\n foldersScanned: string[];\n /** Matching drafts found, before `limit` was applied. */\n count: number;\n truncated: boolean;\n drafts: OutlookDraft[];\n}\n\nexport interface SendDraftsResult {\n sent: number;\n failed: ItemFailure[];\n}\n\nexport interface DeleteDraftsResult {\n deleted: number;\n failed: ItemFailure[];\n}\n\n// ── Deleting mail ────────────────────────────────────────────────────────\nexport interface DeleteMailOptions {\n /** Lift the refusal to touch Inbox and Sent Items (and their subfolders). */\n allowProtected?: boolean;\n /** Resolve and report every id without deleting anything. */\n dryRun?: boolean;\n}\n\n/** What happened to one id in a deleteOutlookEmails call. */\nexport interface DeleteMailOutcome {\n entryId: string;\n subject: string;\n /** The folder the item was actually in — the audit trail for a wrong id. */\n folderPath: string;\n status: 'deleted' | 'would-delete' | 'refused' | 'failed';\n reason: string;\n}\n\nexport interface DeleteMailResult {\n dryRun: boolean;\n deleted: number;\n refused: number;\n failed: number;\n items: DeleteMailOutcome[];\n}\n\nexport interface PurgeDeletedItemsOptions {\n /** Keep items newer than this many days. Default 0: purge everything. */\n olderThanDays?: number;\n /** Count what would be purged without destroying anything. */\n dryRun?: boolean;\n}\n\nexport interface PurgeDeletedItemsResult {\n folderPath: string;\n dryRun: boolean;\n /** Items old enough to qualify. */\n matched: number;\n purged: number;\n /** Items left because they were newer than `olderThanDays`. */\n kept: number;\n failed: number;\n}\n\n// ── Bounces ──────────────────────────────────────────────────────────────\n/** One bounce-back / non-delivery message found in a mailbox. */\nexport interface UndeliverableEmail {\n entryId: string;\n subject: string;\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n /** Which rule flagged it: an NDR message class, a mail-daemon sender, or a bounce subject phrase. */\n matchedReason: string;\n /** Best-effort addresses parsed from the bounce body — the sends that failed. */\n failedRecipients: string[];\n}\n\nexport interface CleanUndeliverableOptions {\n /** How far back to scan. Default 30. */\n daysBack?: number;\n /** Report without deleting. Default TRUE — deleting has to be asked for. */\n dryRun?: boolean;\n}\n\nexport interface CleanUndeliverableResult {\n account: string;\n scannedDays: number;\n dryRun: boolean;\n matchedCount: number;\n deletedCount: number;\n matched: UndeliverableEmail[];\n /** Matched items that could not be deleted (a delete run only). */\n failed: ItemFailure[];\n}\n\nexport interface CollectBouncedRecipientsOptions {\n /** How far back to scan. Default 30. */\n daysBack?: number;\n /** Scan Deleted Items too, so bounces already cleaned away still count. Default true. */\n includeDeletedItems?: boolean;\n}\n\nexport interface SentRecipientGroupsOptions {\n /** How far back to read Sent Items. Default 30. */\n daysBack?: number;\n /** Newest-first cap on messages. Default 3000. */\n limit?: number;\n}\n\n/** One sent message and every SMTP address it went to (To, CC and BCC). */\nexport interface SentRecipientGroup {\n entryId: string;\n subject: string;\n sentOn: string;\n recipients: string[];\n}\n\nexport interface BounceReportOptions {\n /** How far back to read bounce-backs and Sent Items. Default 30. */\n daysBack?: number;\n /** Count bounces already moved to Deleted Items too. Default true. */\n includeDeletedItems?: boolean;\n}\n\n/** A sent message that at least one of its recipients bounced back from. */\nexport interface BouncedSend {\n entryId: string;\n subject: string;\n sentOn: string;\n /** Every address it went to, lower-cased and deduplicated. */\n recipients: string[];\n /** The recipients a bounce-back reported as failed. */\n failedRecipients: string[];\n /** True when every recipient failed: the message reached nobody. */\n allFailed: boolean;\n}\n\nexport interface BounceReport {\n account: string;\n scannedDays: number;\n /** Every distinct address the bounce-backs reported, lower-cased. */\n bouncedAddresses: string[];\n /** The sent messages with a bounced recipient, newest first. */\n sends: BouncedSend[];\n /**\n * Bounced addresses no sent message in the window went to: an alias the\n * server expanded, or a send older than the window.\n */\n unmatchedAddresses: string[];\n}\n\n// ── Reading ──────────────────────────────────────────────────────────────\nexport interface ReadInboxOptions {\n /**\n * Read this folder instead of the Inbox root: a folder under the Inbox by\n * name or path, a well-known folder ('Sent Items', 'Drafts', …), or a full\n * folder path — see mailFolderRef. Only that folder is read, not its\n * subfolders.\n */\n folder?: string;\n /** Only mail from the last this-many days. Default 60. */\n daysBack?: number;\n /** Newest-first cap on rows. Default 50. */\n limit?: number;\n /** Characters of plain-text body per row. Default 600; 0 skips reading bodies. */\n previewChars?: number;\n}\n\nexport interface InboxEmail extends EmailLocator {\n entryId: string;\n storeId: string;\n subject: string;\n /** For outgoing folders (Sent Items, Outbox, Drafts) these carry the\n * RECIPIENT, since a folder of mail \"from\" its own owner says nothing. */\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n bodyPreview: string;\n attachmentNames: string[];\n attachmentCount: number;\n /** The folder the email was read from. */\n folderPath: string;\n}\n\nexport interface InboxSearchFilter {\n /** Only mail from the last this-many days. Default 60 — a scan has no\n * unbounded mode; ask for 365 to reach back a year. */\n daysBack?: number;\n /** Glob-style subject pattern: `*` matches any run, `?` one character,\n * case-insensitive, e.g. '*invoice*'. */\n subjectLike?: string;\n /**\n * Regular expression tested against each subject, case-insensitively\n * whatever the RegExp's own flags. On Windows the pattern runs in .NET's\n * regex engine, so keep to syntax JavaScript and .NET share.\n */\n subjectPattern?: RegExp;\n /** Drop subjects carrying a reply or forward prefix (RE:, FW:, AW:, 回复:, …). */\n excludeReplies?: boolean;\n /** Only return mail with at least one attachment. */\n requireAttachment?: boolean;\n /**\n * Folders under the Inbox to leave out, by full folder path or bare name,\n * case-insensitively. An excluded folder takes its whole subtree with it.\n * Sent, Drafts, Deleted and Junk are always skipped, even where the profile\n * nests them under the Inbox. macOS matches the leaf name only.\n */\n excludeFolders?: readonly string[];\n /**\n * Scan only these folders, given the same way. Matched exactly: naming a\n * folder does not bring its children, and the Inbox root is included only\n * when named. Exclusion wins where the two overlap.\n */\n includeFolders?: readonly string[];\n /** Return each match's full plain-text body. Default true; false makes a\n * scan much lighter when only the metadata is needed. */\n includeBody?: boolean;\n}\n\nexport interface InboxSearchMatch extends EmailLocator {\n entryId: string;\n storeId: string;\n subject: string;\n senderName: string;\n /** SMTP address, resolved from the Exchange DN where the sender is an Exchange user. */\n senderEmail: string;\n receivedTime: string;\n /** The plain-text body; '' when the search asked for no bodies. */\n body: string;\n /** Not saved to disk — pass the ones you want to saveEmailAttachments. */\n attachmentNames: string[];\n folderPath: string;\n}\n\n/**\n * What was kept about an email besides its id, for finding it again once the\n * id no longer resolves: Outlook rewrites an email's id whenever it moves, and\n * an id from one platform means nothing on the other.\n *\n * At least two of `subject`, `sender` and `receivedTime` are needed, since a\n * subject alone names every message in a thread.\n */\nexport interface EmailDescription {\n /** Compared without reply and forward prefixes, spacing or case. */\n subject?: string;\n /**\n * An address, a display name, or both (`Jo Doe <jo@example.com>`). An\n * address is compared as an address; a bare name only against the\n * sender's display name.\n */\n sender?: string;\n /** `yyyy-MM-dd HH:mm` as a listing reported it, or just the day. */\n receivedTime?: string;\n /** The mailbox to search. Default: every account, the one `folderPath` names first. */\n emailAccount?: string;\n /**\n * The folderPath a listing reported (`\\\\team@example.com\\Inbox\\Invoices`).\n * Only decides which mailbox is searched first.\n */\n folderPath?: string;\n}\n\nexport interface LocateEmailOptions {\n /** How far back to search when the description has no receivedTime. Default 60. */\n daysBack?: number;\n}\n\nexport interface SelectedEmail extends EmailLocator {\n entryId: string;\n storeId: string;\n subject: string;\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n body: string;\n /** Not saved to disk — pass the ones you want to saveEmailAttachments. */\n attachmentNames: string[];\n}\n\nexport interface ReadEmailBodyOptions {\n /** Cap on the returned body. Default 8000. */\n maxChars?: number;\n /** Also return the quoted thread below the reply. Default false. */\n includeQuoted?: boolean;\n /** Cap on the quoted thread when it is returned. Default 4000. */\n maxQuotedChars?: number;\n}\n\nexport interface EmailBodyResult {\n entryId: string;\n subject: string;\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n /** The sender's own new text — everything above the quoted thread. */\n body: string;\n /** True when `body` was cut at maxChars. */\n truncated: boolean;\n /** Length of the sender's new text before any cut. */\n bodyLength: number;\n /** What the quoted thread was split on; '' when none was found. */\n quoteSeparator: string;\n /** Length of the quoted thread, reported even when it isn't returned. */\n quotedLength: number;\n /** The quoted thread — only when asked for. */\n quotedOriginal: string;\n attachmentNames: string[];\n attachmentCount: number;\n}\n\n// ── Folders ──────────────────────────────────────────────────────────────\nexport interface MailFolderRef {\n /** olDefaultFolders id of the well-known root the path starts from. */\n rootId: number;\n /** That root as the caller spelled it, for messages. */\n rootLabel: string;\n /** Path segments below the root. */\n segments: string[];\n}\n\nexport interface ListFoldersOptions {\n /** How many levels below the Inbox to list, 1–10. Default 2. */\n maxDepth?: number;\n}\n\nexport interface InboxFolderInfo {\n name: string;\n /** Outlook folder path, e.g. `\\\\mailbox\\Inbox\\Invoices`. */\n folderPath: string;\n itemCount: number;\n /** 1 = direct child of the Inbox, 2 = grandchild, … */\n depth: number;\n}\n\nexport interface MoveEmailsOptions {\n /** Create the destination — the whole missing chain — when absent. Default false. */\n createIfMissing?: boolean;\n}\n\nexport interface MoveEmailsResult {\n folderPath: string;\n folderCreated: boolean;\n moved: number;\n failed: ItemFailure[];\n}\n\n// ── Attachments ──────────────────────────────────────────────────────────\nexport interface SaveAttachmentOptions {\n /**\n * Directory to save into, created if absent. Without one, each call saves\n * into a fresh private directory, so two emails that both carry\n * 'invoice.pdf' can never overwrite each other.\n */\n destDir?: string;\n}\n\nexport interface SavedAttachment {\n /** Where the file was written. A name already taken in the directory gets\n * a numbered suffix rather than overwriting what is there. */\n path: string;\n /** The attachment's name as the email carries it. */\n fileName: string;\n /** Who the email was from — confirm it is the email you meant. */\n subject: string;\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n}\n\n// ── Template emails ──────────────────────────────────────────────────────\nexport interface ReadTemplatesOptions {\n /** Mailbox folder holding the templates, searched up to 3 levels deep. Default 'Templates'. */\n folder?: string;\n /** Return only the template with this subject (case-insensitive). */\n subject?: string;\n /** Return each full HTML body. Default true; false returns previews. */\n includeBody?: boolean;\n /** Newest-first cap on templates, 1–200. Default 20. */\n limit?: number;\n}\n\n/** One template email stored in a mailbox folder. */\nexport interface TemplateEmail {\n entryId: string;\n subject: string;\n /** Full HTML body with embedded (cid:) images removed; '' without includeBody. */\n htmlBody: string;\n /** Short plain-text preview; '' with includeBody. */\n bodyPreview: string;\n /** [[SECTION]] names, returned with or without the body. */\n sections: string[];\n /** `{{PLACEHOLDER}}` names the template expects filled. */\n placeholders: string[];\n lastModified: string;\n}\n\nexport interface TemplateFolderResult {\n folderFound: boolean;\n /** The folder's path when found. */\n folderPath: string;\n templates: TemplateEmail[];\n /** When the folder wasn't found: the mailbox's top-level folder names. */\n availableFolders: string[];\n}\n\nexport interface SaveTemplateParams {\n subject: string;\n htmlBody: string;\n /** Folder to save into, created at the mailbox root if absent. Default 'Templates'. */\n folder?: string;\n}\n\nexport interface SaveTemplateResult {\n folderPath: string;\n folderCreated: boolean;\n}\n\n// ── The contract ─────────────────────────────────────────────────────────\n/**\n * Everything the package does, under one signature per operation whatever the\n * platform. A platform that cannot do something says so at run time — see\n * `CapabilityMap` — rather than through a narrower type.\n */\nexport interface OutlookBridge {\n /** Every mailbox this Outlook profile can reach, as SMTP addresses. */\n getOutlookAccounts(): Promise<string[]>;\n\n /** Send an email, or stage it as a draft. */\n sendOutlookEmail(params: SendEmailParams): Promise<void>;\n\n /** Reply to an email, with the new text above the quoted original. */\n replyOutlookEmail(params: ReplyEmailParams): Promise<ReplyEmailResult>;\n\n /** Recent mail from the Inbox root or one named folder, newest first. */\n readInboxEmails(emailAccount: string, options?: ReadInboxOptions): Promise<InboxEmail[]>;\n\n /** Walk every folder under the Inbox for the mail matching `filter`. */\n searchInboxByFilter(emailAccount: string, filter?: InboxSearchFilter): Promise<InboxSearchMatch[]>;\n\n /** The email currently selected, or open, in Outlook. */\n readSelectedEmail(): Promise<SelectedEmail>;\n\n /** One email's plain-text body, split from the thread it quotes. */\n readEmailBody(email: EmailRef, options?: ReadEmailBodyOptions): Promise<EmailBodyResult>;\n\n /** Open an email in Outlook. */\n openOutlookEmail(email: EmailRef): Promise<void>;\n\n /**\n * Find an email again from what was kept about it, when its id no longer\n * resolves. Searches the Inbox and its subfolders, and returns null rather\n * than a guess: every field the description gives has to agree.\n */\n locateEmail(description: EmailDescription, options?: LocateEmailOptions): Promise<InboxSearchMatch | null>;\n\n /** The folders under an account's Inbox. */\n listInboxFolders(emailAccount: string, options?: ListFoldersOptions): Promise<InboxFolderInfo[]>;\n\n /** File emails into a folder. Moving rewrites each email's entry id. */\n moveOutlookEmails(\n emailAccount: string,\n entryIds: readonly string[],\n folder: string,\n options?: MoveEmailsOptions,\n ): Promise<MoveEmailsResult>;\n\n /** The mail drafts bound to an account, newest first. */\n listOutlookDrafts(emailAccount: string, options?: ListDraftsOptions): Promise<ListDraftsResult>;\n\n /** Send the named drafts — each has to be one of this account's drafts. */\n sendDrafts(emailAccount: string, entryIds: readonly string[]): Promise<SendDraftsResult>;\n\n /** Send every draft bound to an account. */\n sendAllDrafts(emailAccount: string): Promise<SendDraftsResult>;\n\n /** Delete the named drafts (into Deleted Items) — each has to be one of this account's drafts. */\n deleteOutlookDrafts(emailAccount: string, entryIds: readonly string[]): Promise<DeleteDraftsResult>;\n\n /** Delete mail by id into Deleted Items, refusing received and sent mail unless allowed. */\n deleteOutlookEmails(\n emailAccount: string,\n entryIds: readonly string[],\n options?: DeleteMailOptions,\n ): Promise<DeleteMailResult>;\n\n /** Permanently empty an account's Deleted Items. The one irreversible operation. */\n purgeDeletedItems(emailAccount: string, options?: PurgeDeletedItemsOptions): Promise<PurgeDeletedItemsResult>;\n\n /** Save one attachment to disk. */\n saveEmailAttachment(email: EmailRef, fileName: string, options?: SaveAttachmentOptions): Promise<SavedAttachment>;\n\n /** Save several attachments from one email, in the order named. */\n saveEmailAttachments(\n email: EmailRef,\n fileNames: readonly string[],\n options?: SaveAttachmentOptions,\n ): Promise<SavedAttachment[]>;\n\n /** Find bounce-backs in the Inbox and, unless dry-running, delete them. */\n cleanUndeliverableEmails(\n emailAccount: string,\n options?: CleanUndeliverableOptions,\n ): Promise<CleanUndeliverableResult>;\n\n /** The addresses bounce-backs report as having failed, deduplicated. */\n collectBouncedRecipients(emailAccount: string, options?: CollectBouncedRecipientsOptions): Promise<string[]>;\n\n /** Sent Items, each message with the full set of addresses it went to. */\n readSentRecipientGroups(emailAccount: string, options?: SentRecipientGroupsOptions): Promise<SentRecipientGroup[]>;\n\n /** Which sent messages bounced, and for which of their recipients. */\n readBounceReport(emailAccount: string, options?: BounceReportOptions): Promise<BounceReport>;\n\n /** The names of the Outlook signatures on this machine. */\n listOutlookSignatures(): Promise<string[]>;\n\n /** A signature's HTML, or '' when there is no signature by that name. */\n readOutlookSignatureHtml(name: string): Promise<string>;\n\n /** The template emails saved in a mailbox folder. */\n readTemplateEmails(emailAccount: string, options?: ReadTemplatesOptions): Promise<TemplateFolderResult>;\n\n /** Save a new template email. Always adds; never overwrites. */\n saveTemplateEmail(emailAccount: string, template: SaveTemplateParams): Promise<SaveTemplateResult>;\n\n /**\n * Open HTML in an Outlook compose window, wait for the person editing it to\n * save and close, and return what they saved. Has no timeout; cancel it\n * with an AbortSignal.\n */\n editEmailTemplate(label: string, currentHtml: string): Promise<string>;\n}\n\n// ── Capability discovery ─────────────────────────────────────────────────\n/** Every operation whose availability can be asked about. */\nexport type BridgeCapability = keyof OutlookBridge;\n\n/**\n * Which operations work on this machine: all of them on Windows and macOS,\n * none anywhere else. Lets a UI disable what it cannot back instead of\n * discovering the gap when a user clicks.\n */\nexport type CapabilityMap = Readonly<Record<BridgeCapability, boolean>>;\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA,wEAAwE;AACxE,wDAAwD;AACxD,EAAE;AACF,mCAAmC;AACnC,+EAA+E;AAC/E,8EAA8E;AAC9E,YAAY;AACZ,iFAAiF;AACjF,+BAA+B","sourcesContent":["// The public contract: what the package returns, what it takes, and the\n// OutlookBridge interface every entry point answers to.\n//\n// Two conventions hold throughout:\n// - Timestamps are local time as 'yyyy-MM-dd HH:mm', which sorts correctly as\n// a plain string and is what both platforms can produce without a timezone\n// table.\n// - Ids are platform-specific (see EmailLocator). Don't persist one and look it\n// up on the other platform.\n\n// ── Identifying an email ─────────────────────────────────────────────────\n/**\n * Where an email lives, as a listing reported it.\n *\n * Every listing row (`InboxEmail`, `InboxSearchMatch`, `SelectedEmail`, …)\n * carries both fields, so a row can be passed straight back in wherever an\n * `EmailRef` is accepted — which is also the reliable way to keep the two ids\n * from different rows apart.\n */\nexport interface EmailLocator {\n /** Windows: the MAPI EntryID. macOS: Outlook for Mac's integer message id. */\n readonly entryId: string;\n /**\n * Windows: the StoreID the email was listed from, which lets it resolve in\n * any mounted mailbox rather than only the default one. macOS has no\n * equivalent and ignores it.\n */\n readonly storeId?: string;\n}\n\n/** An email to act on: its entry id alone, or anything carrying a locator. */\nexport type EmailRef = string | EmailLocator;\n\n/**\n * Recipients: one address, a comma- or semicolon-separated list, or an array.\n * Display-name forms such as `\"Doe, Jo\" <jo@example.com>` are kept whole.\n */\nexport type Recipients = string | readonly string[];\n\n// ── Sending and replying ─────────────────────────────────────────────────\nexport interface SendEmailParams {\n /** SMTP address of the mailbox to send from. Never falls back to the\n * default account — an address that doesn't resolve is an error. */\n emailAccount: string;\n to?: Recipients;\n cc?: Recipients;\n bcc?: Recipients;\n subject: string;\n htmlBody: string;\n /** Absolute paths of files to attach. Each has to exist. */\n attachments?: readonly string[];\n /**\n * An Outlook signature, by the name listOutlookSignatures reports, added to\n * the body: in its `{{SIGNATURE}}` placeholder when it has one, otherwise\n * at the end. Outlook adds no signature of its own to a body set this way,\n * so this is how a composed email gets one.\n */\n signatureName?: string;\n /** Send now. Default false: the email is staged as a draft instead. */\n sendImmediately?: boolean;\n /**\n * Drafts only. True (the default) opens an Outlook compose window; false\n * files the draft silently into Drafts — the workable choice for batches.\n */\n openDraftWindow?: boolean;\n}\n\nexport interface SendEmailResult {\n /**\n * The draft's entry id when the email was filed silently into Drafts\n * (`openDraftWindow: false`) — the id `listOutlookDrafts` reports for it,\n * so a caller can keep it and later send or delete exactly the drafts it\n * made. '' when the email was sent, or opened in a compose window, which\n * files nothing yet; and on macOS in the rare case Drafts gained another\n * draft with the same subject in the same moment, so this one could not be\n * told apart.\n */\n draftEntryId: string;\n}\n\nexport interface ReplyEmailParams extends EmailLocator {\n /** SMTP address of the mailbox to reply from. */\n emailAccount: string;\n /**\n * HTML inserted above the quoted original (a full document is reduced to\n * its <body> content). Optional when `templateSubject` is given.\n */\n htmlBody?: string;\n /**\n * Reply with a template email saved in the mailbox, found by subject, so\n * the caller never carries a large template's HTML itself.\n */\n templateSubject?: string;\n /** Folder holding the template. Default 'Templates'. */\n templateFolder?: string;\n /**\n * The [[SECTION]] of the template to keep, when one template holds several\n * reply variants (see composeTemplateBody). Required if it has sections.\n */\n templateSection?: string;\n /** `{{PLACEHOLDER}}` → HTML substituted into the composed body. */\n templatePlaceholders?: Readonly<Record<string, string>>;\n /**\n * An Outlook signature (by the name listOutlookSignatures reports), put in\n * the body's `{{SIGNATURE}}` placeholder, or below the new text when there\n * is none. Resolved here, so the signature's HTML never crosses the\n * caller's boundary.\n */\n signatureName?: string;\n /**\n * Answer every recipient of the original instead of only its sender.\n * Default false: widening a reply cannot be taken back, so it is opt-in.\n */\n replyAll?: boolean;\n /** Send now. Default false. */\n sendImmediately?: boolean;\n /** Drafts only: open a compose window (the default) or file silently. */\n openDraftWindow?: boolean;\n}\n\nexport interface ReplyEmailResult {\n /** Who the reply is addressed to. Windows reports Outlook's To line\n * (display names); macOS the resolved addresses. */\n to: string;\n subject: string;\n /** The original email's sender — check it is who you meant to answer. */\n repliedToSender: string;\n}\n\n// ── Per-item outcomes ────────────────────────────────────────────────────\n/** One item a batch operation could not handle, and why. */\nexport interface ItemFailure {\n entryId: string;\n /** The item's subject when it could be read, else ''. */\n subject: string;\n error: string;\n}\n\n// ── Drafts ───────────────────────────────────────────────────────────────\n/** One mail draft belonging to an account. */\nexport interface OutlookDraft {\n entryId: string;\n subject: string;\n /** The To line as Outlook renders it. */\n to: string;\n /** Recipient addresses, best effort — an unresolved Exchange entry falls back to its name. */\n toEmails: string[];\n /** Short plain-text preview, enough to tell similar drafts apart. */\n bodyPreview: string;\n hasAttachments: boolean;\n lastModified: string;\n /** The Drafts folder it sits in; one account's drafts can span two. */\n folderPath: string;\n}\n\nexport interface ListDraftsOptions {\n /** Newest-first cap on the drafts returned. Default 100. */\n limit?: number;\n /** Characters of body preview per draft. Default 300; 0 omits it. */\n previewChars?: number;\n}\n\nexport interface ListDraftsResult {\n account: string;\n /** The Drafts folders scanned, in scan order. */\n foldersScanned: string[];\n /** Matching drafts found, before `limit` was applied. */\n count: number;\n truncated: boolean;\n drafts: OutlookDraft[];\n}\n\nexport interface SendDraftsResult {\n sent: number;\n failed: ItemFailure[];\n}\n\nexport interface DeleteDraftsResult {\n deleted: number;\n failed: ItemFailure[];\n}\n\n// ── Deleting mail ────────────────────────────────────────────────────────\nexport interface DeleteMailOptions {\n /** Lift the refusal to touch Inbox and Sent Items (and their subfolders). */\n allowProtected?: boolean;\n /** Resolve and report every id without deleting anything. */\n dryRun?: boolean;\n}\n\n/** What happened to one id in a deleteOutlookEmails call. */\nexport interface DeleteMailOutcome {\n entryId: string;\n subject: string;\n /** The folder the item was actually in — the audit trail for a wrong id. */\n folderPath: string;\n status: 'deleted' | 'would-delete' | 'refused' | 'failed';\n reason: string;\n}\n\nexport interface DeleteMailResult {\n dryRun: boolean;\n deleted: number;\n refused: number;\n failed: number;\n items: DeleteMailOutcome[];\n}\n\nexport interface PurgeDeletedItemsOptions {\n /** Keep items newer than this many days. Default 0: purge everything. */\n olderThanDays?: number;\n /** Count what would be purged without destroying anything. */\n dryRun?: boolean;\n}\n\nexport interface PurgeDeletedItemsResult {\n folderPath: string;\n dryRun: boolean;\n /** Items old enough to qualify. */\n matched: number;\n purged: number;\n /** Items left because they were newer than `olderThanDays`. */\n kept: number;\n failed: number;\n}\n\n// ── Bounces ──────────────────────────────────────────────────────────────\n/** One bounce-back / non-delivery message found in a mailbox. */\nexport interface UndeliverableEmail {\n entryId: string;\n subject: string;\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n /** Which rule flagged it: an NDR message class, a mail-daemon sender, or a bounce subject phrase. */\n matchedReason: string;\n /** Best-effort addresses parsed from the bounce body — the sends that failed. */\n failedRecipients: string[];\n}\n\nexport interface CleanUndeliverableOptions {\n /** How far back to scan. Default 30. */\n daysBack?: number;\n /** Report without deleting. Default TRUE — deleting has to be asked for. */\n dryRun?: boolean;\n}\n\nexport interface CleanUndeliverableResult {\n account: string;\n scannedDays: number;\n dryRun: boolean;\n matchedCount: number;\n deletedCount: number;\n matched: UndeliverableEmail[];\n /** Matched items that could not be deleted (a delete run only). */\n failed: ItemFailure[];\n}\n\nexport interface CollectBouncedRecipientsOptions {\n /** How far back to scan. Default 30. */\n daysBack?: number;\n /** Scan Deleted Items too, so bounces already cleaned away still count. Default true. */\n includeDeletedItems?: boolean;\n}\n\nexport interface SentRecipientGroupsOptions {\n /** How far back to read Sent Items. Default 30. */\n daysBack?: number;\n /** Newest-first cap on messages. Default 3000. */\n limit?: number;\n}\n\n/** One sent message and every SMTP address it went to (To, CC and BCC). */\nexport interface SentRecipientGroup {\n entryId: string;\n subject: string;\n sentOn: string;\n recipients: string[];\n}\n\nexport interface BounceReportOptions {\n /** How far back to read bounce-backs and Sent Items. Default 30. */\n daysBack?: number;\n /** Count bounces already moved to Deleted Items too. Default true. */\n includeDeletedItems?: boolean;\n}\n\n/** A sent message that at least one of its recipients bounced back from. */\nexport interface BouncedSend {\n entryId: string;\n subject: string;\n sentOn: string;\n /** Every address it went to, lower-cased and deduplicated. */\n recipients: string[];\n /** The recipients a bounce-back reported as failed. */\n failedRecipients: string[];\n /** True when every recipient failed: the message reached nobody. */\n allFailed: boolean;\n}\n\nexport interface BounceReport {\n account: string;\n scannedDays: number;\n /** Every distinct address the bounce-backs reported, lower-cased. */\n bouncedAddresses: string[];\n /** The sent messages with a bounced recipient, newest first. */\n sends: BouncedSend[];\n /**\n * Bounced addresses no sent message in the window went to: an alias the\n * server expanded, or a send older than the window.\n */\n unmatchedAddresses: string[];\n}\n\n// ── Reading ──────────────────────────────────────────────────────────────\nexport interface ReadInboxOptions {\n /**\n * Read this folder instead of the Inbox root: a folder under the Inbox by\n * name or path, a well-known folder ('Sent Items', 'Drafts', …), or a full\n * folder path — see mailFolderRef. Only that folder is read, not its\n * subfolders.\n */\n folder?: string;\n /** Only mail from the last this-many days. Default 60. */\n daysBack?: number;\n /** Newest-first cap on rows. Default 50. */\n limit?: number;\n /** Characters of plain-text body per row. Default 600; 0 skips reading bodies. */\n previewChars?: number;\n}\n\nexport interface InboxEmail extends EmailLocator {\n entryId: string;\n storeId: string;\n subject: string;\n /** For outgoing folders (Sent Items, Outbox, Drafts) these carry the\n * RECIPIENT, since a folder of mail \"from\" its own owner says nothing. */\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n bodyPreview: string;\n attachmentNames: string[];\n attachmentCount: number;\n /** The folder the email was read from. */\n folderPath: string;\n}\n\nexport interface InboxSearchFilter {\n /** Only mail from the last this-many days. Default 60 — a scan has no\n * unbounded mode; ask for 365 to reach back a year. */\n daysBack?: number;\n /** Glob-style subject pattern: `*` matches any run, `?` one character,\n * case-insensitive, e.g. '*invoice*'. */\n subjectLike?: string;\n /**\n * Regular expression tested against each subject, case-insensitively\n * whatever the RegExp's own flags; on macOS a `u` or `v` flag is kept,\n * since it decides how the pattern parses. On Windows the pattern runs in\n * .NET's regex engine, so keep to syntax JavaScript and .NET share.\n */\n subjectPattern?: RegExp;\n /** Drop subjects carrying a reply or forward prefix (RE:, FW:, AW:, 回复:, …). */\n excludeReplies?: boolean;\n /** Only return mail with at least one attachment. */\n requireAttachment?: boolean;\n /**\n * Folders under the Inbox to leave out, by full folder path or bare name,\n * case-insensitively. An excluded folder takes its whole subtree with it.\n * Sent, Drafts, Deleted and Junk are always skipped, even where the profile\n * nests them under the Inbox. macOS matches the leaf name only.\n */\n excludeFolders?: readonly string[];\n /**\n * Scan only these folders, given the same way. Matched exactly: naming a\n * folder does not bring its children, and the Inbox root is included only\n * when named. Exclusion wins where the two overlap.\n */\n includeFolders?: readonly string[];\n /** Return each match's full plain-text body. Default true; false makes a\n * scan much lighter when only the metadata is needed. */\n includeBody?: boolean;\n}\n\nexport interface InboxSearchMatch extends EmailLocator {\n entryId: string;\n storeId: string;\n subject: string;\n senderName: string;\n /** SMTP address, resolved from the Exchange DN where the sender is an Exchange user. */\n senderEmail: string;\n receivedTime: string;\n /** The plain-text body; '' when the search asked for no bodies. */\n body: string;\n /** Not saved to disk — pass the ones you want to saveEmailAttachments. */\n attachmentNames: string[];\n folderPath: string;\n}\n\n/**\n * What was kept about an email besides its id, for finding it again once the\n * id no longer resolves: Outlook rewrites an email's id whenever it moves, and\n * an id from one platform means nothing on the other.\n *\n * At least two of `subject`, `sender` and `receivedTime` are needed, since a\n * subject alone names every message in a thread.\n */\nexport interface EmailDescription {\n /** Compared without reply and forward prefixes, spacing or case. */\n subject?: string;\n /**\n * An address, a display name, or both (`Jo Doe <jo@example.com>`). An\n * address is compared as an address; a bare name only against the\n * sender's display name.\n */\n sender?: string;\n /** `yyyy-MM-dd HH:mm` as a listing reported it, or just the day. */\n receivedTime?: string;\n /** The mailbox to search. Default: every account, the one `folderPath` names first. */\n emailAccount?: string;\n /**\n * The folderPath a listing reported (`\\\\team@example.com\\Inbox\\Invoices`).\n * Only decides which mailbox is searched first.\n */\n folderPath?: string;\n}\n\nexport interface LocateEmailOptions {\n /** How far back to search when the description has no receivedTime. Default 60. */\n daysBack?: number;\n}\n\nexport interface SelectedEmail extends EmailLocator {\n entryId: string;\n storeId: string;\n subject: string;\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n body: string;\n /** Not saved to disk — pass the ones you want to saveEmailAttachments. */\n attachmentNames: string[];\n /**\n * The folder it sits in, in the form the listings report\n * (`\\\\team@example.com\\[Gmail]\\Drafts`) and so naming its mailbox. '' when\n * Outlook can't say: an email opened from a file, or on macOS one whose\n * mailbox neither AppleScript nor the profile database names.\n */\n folderPath: string;\n}\n\nexport interface ReadEmailBodyOptions {\n /** Cap on the returned body. Default 8000. */\n maxChars?: number;\n /** Also return the quoted thread below the reply. Default false. */\n includeQuoted?: boolean;\n /** Cap on the quoted thread when it is returned. Default 4000. */\n maxQuotedChars?: number;\n}\n\nexport interface EmailBodyResult {\n entryId: string;\n subject: string;\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n /** The sender's own new text — everything above the quoted thread. */\n body: string;\n /** True when `body` was cut at maxChars. */\n truncated: boolean;\n /** Length of the sender's new text before any cut. */\n bodyLength: number;\n /** What the quoted thread was split on; '' when none was found. */\n quoteSeparator: string;\n /** Length of the quoted thread, reported even when it isn't returned. */\n quotedLength: number;\n /** The quoted thread — only when asked for. */\n quotedOriginal: string;\n attachmentNames: string[];\n attachmentCount: number;\n}\n\n// ── Folders ──────────────────────────────────────────────────────────────\nexport interface MailFolderRef {\n /** olDefaultFolders id of the well-known root the path starts from. */\n rootId: number;\n /** That root as the caller spelled it, for messages. */\n rootLabel: string;\n /** Path segments below the root. */\n segments: string[];\n}\n\nexport interface ListFoldersOptions {\n /** How many levels below the Inbox to list, 1–10. Default 2. */\n maxDepth?: number;\n}\n\nexport interface InboxFolderInfo {\n name: string;\n /** Outlook folder path, e.g. `\\\\mailbox\\Inbox\\Invoices`. */\n folderPath: string;\n itemCount: number;\n /** 1 = direct child of the Inbox, 2 = grandchild, … */\n depth: number;\n}\n\nexport interface MoveEmailsOptions {\n /** Create the destination — the whole missing chain — when absent. Default false. */\n createIfMissing?: boolean;\n}\n\nexport interface MoveEmailsResult {\n folderPath: string;\n folderCreated: boolean;\n moved: number;\n failed: ItemFailure[];\n}\n\n// ── Attachments ──────────────────────────────────────────────────────────\nexport interface SaveAttachmentOptions {\n /**\n * Directory to save into, created if absent. Without one, each call saves\n * into a fresh private directory, so two emails that both carry\n * 'invoice.pdf' can never overwrite each other.\n */\n destDir?: string;\n}\n\nexport interface SavedAttachment {\n /** Where the file was written. A name already taken in the directory gets\n * a numbered suffix rather than overwriting what is there. */\n path: string;\n /** The attachment's name as the email carries it. */\n fileName: string;\n /** Who the email was from — confirm it is the email you meant. */\n subject: string;\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n}\n\n// ── Template emails ──────────────────────────────────────────────────────\nexport interface ReadTemplatesOptions {\n /** Mailbox folder holding the templates, searched up to 3 levels deep. Default 'Templates'. */\n folder?: string;\n /** Return only the template with this subject (case-insensitive). */\n subject?: string;\n /** Return each full HTML body. Default true; false returns previews. */\n includeBody?: boolean;\n /** Newest-first cap on templates, 1–200. Default 20. */\n limit?: number;\n}\n\n/** One template email stored in a mailbox folder. */\nexport interface TemplateEmail {\n entryId: string;\n subject: string;\n /** Full HTML body with embedded (cid:) images removed; '' without includeBody. */\n htmlBody: string;\n /** Short plain-text preview; '' with includeBody. */\n bodyPreview: string;\n /** [[SECTION]] names, returned with or without the body. */\n sections: string[];\n /** `{{PLACEHOLDER}}` names the template expects filled. */\n placeholders: string[];\n lastModified: string;\n}\n\nexport interface TemplateFolderResult {\n folderFound: boolean;\n /** The folder's path when found. */\n folderPath: string;\n templates: TemplateEmail[];\n /** When the folder wasn't found: the mailbox's top-level folder names. */\n availableFolders: string[];\n}\n\nexport interface SaveTemplateParams {\n subject: string;\n htmlBody: string;\n /** Folder to save into, created at the mailbox root if absent. Default 'Templates'. */\n folder?: string;\n}\n\nexport interface SaveTemplateResult {\n folderPath: string;\n folderCreated: boolean;\n}\n\n// ── The contract ─────────────────────────────────────────────────────────\n/**\n * Everything the package does, under one signature per operation whatever the\n * platform. A platform that cannot do something says so at run time — see\n * `CapabilityMap` — rather than through a narrower type.\n */\nexport interface OutlookBridge {\n /** Every mailbox this Outlook profile can reach, as SMTP addresses. */\n getOutlookAccounts(): Promise<string[]>;\n\n /** Send an email, or stage it as a draft. */\n sendOutlookEmail(params: SendEmailParams): Promise<SendEmailResult>;\n\n /** Reply to an email, with the new text above the quoted original. */\n replyOutlookEmail(params: ReplyEmailParams): Promise<ReplyEmailResult>;\n\n /** Recent mail from the Inbox root or one named folder, newest first. */\n readInboxEmails(emailAccount: string, options?: ReadInboxOptions): Promise<InboxEmail[]>;\n\n /** Walk every folder under the Inbox for the mail matching `filter`. */\n searchInboxByFilter(emailAccount: string, filter?: InboxSearchFilter): Promise<InboxSearchMatch[]>;\n\n /** The email currently selected, or open, in Outlook. */\n readSelectedEmail(): Promise<SelectedEmail>;\n\n /** One email's plain-text body, split from the thread it quotes. */\n readEmailBody(email: EmailRef, options?: ReadEmailBodyOptions): Promise<EmailBodyResult>;\n\n /** Open an email in Outlook. */\n openOutlookEmail(email: EmailRef): Promise<void>;\n\n /**\n * Find an email again from what was kept about it, when its id no longer\n * resolves. Searches the Inbox and its subfolders, and returns null rather\n * than a guess: every field the description gives has to agree.\n */\n locateEmail(description: EmailDescription, options?: LocateEmailOptions): Promise<InboxSearchMatch | null>;\n\n /** The folders under an account's Inbox. */\n listInboxFolders(emailAccount: string, options?: ListFoldersOptions): Promise<InboxFolderInfo[]>;\n\n /** File emails into a folder. Moving rewrites each email's entry id. */\n moveOutlookEmails(\n emailAccount: string,\n entryIds: readonly string[],\n folder: string,\n options?: MoveEmailsOptions,\n ): Promise<MoveEmailsResult>;\n\n /** The mail drafts bound to an account, newest first. */\n listOutlookDrafts(emailAccount: string, options?: ListDraftsOptions): Promise<ListDraftsResult>;\n\n /** Send the named drafts — each has to be one of this account's drafts. */\n sendDrafts(emailAccount: string, entryIds: readonly string[]): Promise<SendDraftsResult>;\n\n /** Send every draft bound to an account. */\n sendAllDrafts(emailAccount: string): Promise<SendDraftsResult>;\n\n /** Delete the named drafts (into Deleted Items) — each has to be one of this account's drafts. */\n deleteOutlookDrafts(emailAccount: string, entryIds: readonly string[]): Promise<DeleteDraftsResult>;\n\n /** Delete mail by id into Deleted Items, refusing received and sent mail unless allowed. */\n deleteOutlookEmails(\n emailAccount: string,\n entryIds: readonly string[],\n options?: DeleteMailOptions,\n ): Promise<DeleteMailResult>;\n\n /** Permanently empty an account's Deleted Items. The one irreversible operation. */\n purgeDeletedItems(emailAccount: string, options?: PurgeDeletedItemsOptions): Promise<PurgeDeletedItemsResult>;\n\n /** Save one attachment to disk. */\n saveEmailAttachment(email: EmailRef, fileName: string, options?: SaveAttachmentOptions): Promise<SavedAttachment>;\n\n /** Save several attachments from one email, in the order named. */\n saveEmailAttachments(\n email: EmailRef,\n fileNames: readonly string[],\n options?: SaveAttachmentOptions,\n ): Promise<SavedAttachment[]>;\n\n /** Find bounce-backs in the Inbox and, unless dry-running, delete them. */\n cleanUndeliverableEmails(\n emailAccount: string,\n options?: CleanUndeliverableOptions,\n ): Promise<CleanUndeliverableResult>;\n\n /** The addresses bounce-backs report as having failed, deduplicated. */\n collectBouncedRecipients(emailAccount: string, options?: CollectBouncedRecipientsOptions): Promise<string[]>;\n\n /** Sent Items, each message with the full set of addresses it went to. */\n readSentRecipientGroups(emailAccount: string, options?: SentRecipientGroupsOptions): Promise<SentRecipientGroup[]>;\n\n /** Which sent messages bounced, and for which of their recipients. */\n readBounceReport(emailAccount: string, options?: BounceReportOptions): Promise<BounceReport>;\n\n /** The names of the Outlook signatures on this machine. */\n listOutlookSignatures(): Promise<string[]>;\n\n /** A signature's HTML, or '' when there is no signature by that name. */\n readOutlookSignatureHtml(name: string): Promise<string>;\n\n /** The template emails saved in a mailbox folder. */\n readTemplateEmails(emailAccount: string, options?: ReadTemplatesOptions): Promise<TemplateFolderResult>;\n\n /** Save a new template email. Always adds; never overwrites. */\n saveTemplateEmail(emailAccount: string, template: SaveTemplateParams): Promise<SaveTemplateResult>;\n\n /**\n * Open HTML in an Outlook compose window, wait for the person editing it to\n * save and close, and return what they saved. Has no timeout; cancel it\n * with an AbortSignal.\n */\n editEmailTemplate(label: string, currentHtml: string): Promise<string>;\n}\n\n// ── Capability discovery ─────────────────────────────────────────────────\n/** Every operation whose availability can be asked about. */\nexport type BridgeCapability = keyof OutlookBridge;\n\n/**\n * Which operations work on this machine: all of them on Windows and macOS,\n * none anywhere else. Lets a UI disable what it cannot back instead of\n * discovering the gap when a user clicks.\n */\nexport type CapabilityMap = Readonly<Record<BridgeCapability, boolean>>;\n"]}
|
package/dist/windows/bounces.js
CHANGED
|
@@ -86,7 +86,7 @@ ${bounceClassifierScript()}
|
|
|
86
86
|
$dryRun = ${(0, run_1.psBool)(request.dryRun)}
|
|
87
87
|
$inbox = $store.GetDefaultFolder(${(0, run_1.psInt)(mail_1.FolderId.Inbox)})
|
|
88
88
|
${(0, scripts_1.cutoffScript)(request.daysBack)}
|
|
89
|
-
${(0, scripts_1.itemsSinceScript)('inbox', 'ReceivedTime'
|
|
89
|
+
${(0, scripts_1.itemsSinceScript)('inbox', 'ReceivedTime')}
|
|
90
90
|
$reports = @()
|
|
91
91
|
$matchedItems = @()
|
|
92
92
|
$count = $filtered.Count
|
|
@@ -111,7 +111,7 @@ if (-not $dryRun) {
|
|
|
111
111
|
try { $entryId = [string]$m.EntryID } catch {}
|
|
112
112
|
try { $subject = [string]$m.Subject } catch {}
|
|
113
113
|
try { $m.Delete(); $deleted++ }
|
|
114
|
-
catch { $
|
|
114
|
+
catch { ${(0, json_1.itemFailurePs)('$entryId')} }
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
ConvertTo-Json -Depth 4 -InputObject ([PSCustomObject]@{
|
|
@@ -159,7 +159,7 @@ foreach ($role in @(${roles.map(run_1.psInt).join(', ')})) {
|
|
|
159
159
|
$scanFolder = $null
|
|
160
160
|
try { $scanFolder = $store.GetDefaultFolder($role) } catch {}
|
|
161
161
|
if ($scanFolder -eq $null) { continue }
|
|
162
|
-
${(0, scripts_1.itemsSinceScript)('scanFolder', 'ReceivedTime'
|
|
162
|
+
${(0, scripts_1.itemsSinceScript)('scanFolder', 'ReceivedTime').trim()}
|
|
163
163
|
$count = $filtered.Count
|
|
164
164
|
for ($i = 1; $i -le $count; $i++) {
|
|
165
165
|
$item = $null
|
|
@@ -186,7 +186,7 @@ async function readSentRecipientGroups(request) {
|
|
|
186
186
|
${scripts_1.DELIVERY_STORE_PS}
|
|
187
187
|
$sentFolder = $store.GetDefaultFolder(${(0, run_1.psInt)(mail_1.FolderId.SentMail)})
|
|
188
188
|
${(0, scripts_1.cutoffScript)(request.daysBack)}
|
|
189
|
-
${(0, scripts_1.itemsSinceScript)('sentFolder', 'SentOn'
|
|
189
|
+
${(0, scripts_1.itemsSinceScript)('sentFolder', 'SentOn')}
|
|
190
190
|
$filtered.Sort('[SentOn]', $true)
|
|
191
191
|
$limit = ${(0, run_1.psInt)(request.limit)}
|
|
192
192
|
$rows = @()
|