@dawnlit/outlook-bridge 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +253 -0
- package/dist/OutlookService.d.ts +43 -0
- package/dist/OutlookService.js +137 -0
- package/dist/OutlookService.js.map +1 -0
- package/dist/errors.d.ts +112 -0
- package/dist/errors.js +140 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +71 -0
- package/dist/index.js.map +1 -0
- package/dist/mac/accounts.d.ts +5 -0
- package/dist/mac/accounts.js +17 -0
- package/dist/mac/accounts.js.map +1 -0
- package/dist/mac/attachments.d.ts +28 -0
- package/dist/mac/attachments.js +120 -0
- package/dist/mac/attachments.js.map +1 -0
- package/dist/mac/bounces.d.ts +32 -0
- package/dist/mac/bounces.js +285 -0
- package/dist/mac/bounces.js.map +1 -0
- package/dist/mac/cleanup.d.ts +29 -0
- package/dist/mac/cleanup.js +219 -0
- package/dist/mac/cleanup.js.map +1 -0
- package/dist/mac/drafts.d.ts +27 -0
- package/dist/mac/drafts.js +214 -0
- package/dist/mac/drafts.js.map +1 -0
- package/dist/mac/folders.d.ts +28 -0
- package/dist/mac/folders.js +127 -0
- package/dist/mac/folders.js.map +1 -0
- package/dist/mac/index.d.ts +22 -0
- package/dist/mac/index.js +119 -0
- package/dist/mac/index.js.map +1 -0
- package/dist/mac/read.d.ts +46 -0
- package/dist/mac/read.js +406 -0
- package/dist/mac/read.js.map +1 -0
- package/dist/mac/run.d.ts +64 -0
- package/dist/mac/run.js +250 -0
- package/dist/mac/run.js.map +1 -0
- package/dist/mac/scripts.d.ts +137 -0
- package/dist/mac/scripts.js +337 -0
- package/dist/mac/scripts.js.map +1 -0
- package/dist/mac/send.d.ts +23 -0
- package/dist/mac/send.js +135 -0
- package/dist/mac/send.js.map +1 -0
- package/dist/mac/signatures.d.ts +4 -0
- package/dist/mac/signatures.js +36 -0
- package/dist/mac/signatures.js.map +1 -0
- package/dist/mac/templates.d.ts +34 -0
- package/dist/mac/templates.js +266 -0
- package/dist/mac/templates.js.map +1 -0
- package/dist/mail.d.ts +72 -0
- package/dist/mail.js +131 -0
- package/dist/mail.js.map +1 -0
- package/dist/outlookTemplateSections.d.ts +62 -0
- package/dist/outlookTemplateSections.js +313 -0
- package/dist/outlookTemplateSections.js.map +1 -0
- package/dist/runtime.d.ts +76 -0
- package/dist/runtime.js +123 -0
- package/dist/runtime.js.map +1 -0
- package/dist/shared/attachmentMatch.d.ts +32 -0
- package/dist/shared/attachmentMatch.js +50 -0
- package/dist/shared/attachmentMatch.js.map +1 -0
- package/dist/shared/bounceRules.d.ts +33 -0
- package/dist/shared/bounceRules.js +104 -0
- package/dist/shared/bounceRules.js.map +1 -0
- package/dist/shared/capabilities.d.ts +3 -0
- package/dist/shared/capabilities.js +9 -0
- package/dist/shared/capabilities.js.map +1 -0
- package/dist/shared/json.d.ts +11 -0
- package/dist/shared/json.js +45 -0
- package/dist/shared/json.js.map +1 -0
- package/dist/shared/replyBody.d.ts +49 -0
- package/dist/shared/replyBody.js +113 -0
- package/dist/shared/replyBody.js.map +1 -0
- package/dist/types.d.ts +365 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/dist/windows/accounts.d.ts +2 -0
- package/dist/windows/accounts.js +22 -0
- package/dist/windows/accounts.js.map +1 -0
- package/dist/windows/attachments.d.ts +28 -0
- package/dist/windows/attachments.js +141 -0
- package/dist/windows/attachments.js.map +1 -0
- package/dist/windows/bounces.d.ts +34 -0
- package/dist/windows/bounces.js +267 -0
- package/dist/windows/bounces.js.map +1 -0
- package/dist/windows/cleanup.d.ts +29 -0
- package/dist/windows/cleanup.js +165 -0
- package/dist/windows/cleanup.js.map +1 -0
- package/dist/windows/drafts.d.ts +29 -0
- package/dist/windows/drafts.js +253 -0
- package/dist/windows/drafts.js.map +1 -0
- package/dist/windows/folders.d.ts +16 -0
- package/dist/windows/folders.js +88 -0
- package/dist/windows/folders.js.map +1 -0
- package/dist/windows/index.d.ts +27 -0
- package/dist/windows/index.js +100 -0
- package/dist/windows/index.js.map +1 -0
- package/dist/windows/read.d.ts +54 -0
- package/dist/windows/read.js +372 -0
- package/dist/windows/read.js.map +1 -0
- package/dist/windows/run.d.ts +54 -0
- package/dist/windows/run.js +161 -0
- package/dist/windows/run.js.map +1 -0
- package/dist/windows/scripts.d.ts +56 -0
- package/dist/windows/scripts.js +163 -0
- package/dist/windows/scripts.js.map +1 -0
- package/dist/windows/send.d.ts +11 -0
- package/dist/windows/send.js +114 -0
- package/dist/windows/send.js.map +1 -0
- package/dist/windows/signatures.d.ts +14 -0
- package/dist/windows/signatures.js +69 -0
- package/dist/windows/signatures.js.map +1 -0
- package/dist/windows/templates.d.ts +25 -0
- package/dist/windows/templates.js +246 -0
- package/dist/windows/templates.js.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FIND_FOLDER_PS = exports.SENDER_SMTP_PS = exports.DELIVERY_STORE_PS = void 0;
|
|
4
|
+
exports.accountScript = accountScript;
|
|
5
|
+
exports.namedStoreScript = namedStoreScript;
|
|
6
|
+
exports.getItemScript = getItemScript;
|
|
7
|
+
exports.mailScopeScript = mailScopeScript;
|
|
8
|
+
// The PowerShell fragments more than one Windows operation is built from.
|
|
9
|
+
//
|
|
10
|
+
// A fragment lives here once at least two modules emit it; one used by a single
|
|
11
|
+
// operation stays beside that operation, where its rules are readable next to
|
|
12
|
+
// the code that depends on them.
|
|
13
|
+
const run_1 = require("./run");
|
|
14
|
+
/**
|
|
15
|
+
* Open a MAPI session. `$outlook` and `$ns` are what every later fragment reads.
|
|
16
|
+
*
|
|
17
|
+
* `Logon()` is a no-op against an Outlook that is already running and starts one
|
|
18
|
+
* that isn't, so it costs nothing and removes a class of "works only when
|
|
19
|
+
* Outlook happens to be open" failure.
|
|
20
|
+
*/
|
|
21
|
+
const PS_PRELUDE = `
|
|
22
|
+
$ErrorActionPreference = 'Stop'
|
|
23
|
+
$outlook = New-Object -ComObject Outlook.Application
|
|
24
|
+
$ns = $outlook.GetNamespace('mapi')
|
|
25
|
+
$ns.Logon()
|
|
26
|
+
`;
|
|
27
|
+
/**
|
|
28
|
+
* Session plus the account whose SMTP address matches; binds `$target` and
|
|
29
|
+
* `$account`.
|
|
30
|
+
*
|
|
31
|
+
* Fails rather than falling back to the default account. A bad address must
|
|
32
|
+
* never silently send from — or read — the wrong mailbox, and the thrown
|
|
33
|
+
* sentence is the one `classifyRunFailure` recognises to raise
|
|
34
|
+
* `AccountNotFoundError`, so the wording is load-bearing.
|
|
35
|
+
*/
|
|
36
|
+
function accountScript(emailAccount) {
|
|
37
|
+
return `${PS_PRELUDE}
|
|
38
|
+
$target = '${(0, run_1.psEscape)(emailAccount)}'
|
|
39
|
+
$account = $null
|
|
40
|
+
foreach ($a in $ns.Accounts) {
|
|
41
|
+
if ($a.SmtpAddress -ieq $target) { $account = $a; break }
|
|
42
|
+
}
|
|
43
|
+
if ($account -eq $null) { throw "Account '$target' not found" }
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
/** The account's own delivery store, as `$store`. Requires `accountScript`. */
|
|
47
|
+
exports.DELIVERY_STORE_PS = `
|
|
48
|
+
$store = $account.DeliveryStore
|
|
49
|
+
$storeId = $store.StoreID
|
|
50
|
+
`;
|
|
51
|
+
/**
|
|
52
|
+
* The account's store reached through the namespace's top-level folders, as
|
|
53
|
+
* `$storeFolder` / `$store`.
|
|
54
|
+
*
|
|
55
|
+
* Kept alongside `DELIVERY_STORE_PS` rather than replaced by it: the reading
|
|
56
|
+
* operations resolve the store this way, and on a profile where a mailbox is
|
|
57
|
+
* open under a display name that differs from the delivery store's, the two do
|
|
58
|
+
* not select the same folders. Changing which one a reader uses changes what it
|
|
59
|
+
* returns, so each keeps the route it was verified against.
|
|
60
|
+
*/
|
|
61
|
+
function namedStoreScript(emailAccount) {
|
|
62
|
+
return `
|
|
63
|
+
$storeFolder = $null
|
|
64
|
+
foreach ($f in $ns.Folders) {
|
|
65
|
+
if ($f.Name -ieq $account.DisplayName) { $storeFolder = $f; break }
|
|
66
|
+
}
|
|
67
|
+
if ($storeFolder -eq $null) { throw "Store folder not found for account '${(0, run_1.psEscape)(emailAccount)}'" }
|
|
68
|
+
$store = $storeFolder.Store
|
|
69
|
+
$storeId = $store.StoreID
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolve one item into `$item`. A StoreID disambiguates across mailboxes, so
|
|
74
|
+
* pass it whenever the listing that produced the id carried one.
|
|
75
|
+
*/
|
|
76
|
+
function getItemScript(entryId, storeId) {
|
|
77
|
+
return storeId
|
|
78
|
+
? `$item = $ns.GetItemFromID('${(0, run_1.psEscape)(entryId)}', '${(0, run_1.psEscape)(storeId)}')`
|
|
79
|
+
: `$item = $ns.GetItemFromID('${(0, run_1.psEscape)(entryId)}')`;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The sender's real SMTP address, as `$senderSmtp`. Requires `$item`.
|
|
83
|
+
*
|
|
84
|
+
* An Exchange sender's `SenderEmailAddress` is an X500 DN rather than an
|
|
85
|
+
* address; a caller that means to reply to it, or correlate it with anything,
|
|
86
|
+
* needs the address it resolves to.
|
|
87
|
+
*/
|
|
88
|
+
exports.SENDER_SMTP_PS = `
|
|
89
|
+
$senderSmtp = ''
|
|
90
|
+
try {
|
|
91
|
+
if ($item.SenderEmailType -eq 'EX') { $senderSmtp = $item.Sender.GetExchangeUser().PrimarySmtpAddress }
|
|
92
|
+
} catch {}
|
|
93
|
+
if (-not $senderSmtp) { $senderSmtp = if ($item.SenderEmailAddress) { $item.SenderEmailAddress } else { '' } }
|
|
94
|
+
`;
|
|
95
|
+
/**
|
|
96
|
+
* Recursive folder-by-name search. Case-insensitive, depth-capped so a huge
|
|
97
|
+
* mailbox tree can't hang the scan.
|
|
98
|
+
*/
|
|
99
|
+
exports.FIND_FOLDER_PS = `
|
|
100
|
+
function Find-FolderByName($root, $name, $depth) {
|
|
101
|
+
foreach ($f in $root.Folders) {
|
|
102
|
+
if ($f.Name -ieq $name) { return $f }
|
|
103
|
+
}
|
|
104
|
+
if ($depth -le 1) { return $null }
|
|
105
|
+
foreach ($f in $root.Folders) {
|
|
106
|
+
$hit = Find-FolderByName $f $name ($depth - 1)
|
|
107
|
+
if ($hit -ne $null) { return $hit }
|
|
108
|
+
}
|
|
109
|
+
return $null
|
|
110
|
+
}
|
|
111
|
+
`;
|
|
112
|
+
/**
|
|
113
|
+
* Emit the PowerShell that resolves `$scope` from `$store`, walking a well-known
|
|
114
|
+
* root down through any further segments. Also binds `$scopeCreated`.
|
|
115
|
+
*
|
|
116
|
+
* Split out so the emitted script can be parse-checked without an Outlook
|
|
117
|
+
* session — the folder walk is the only part that varies per call, and a syntax
|
|
118
|
+
* error in it would only ever surface as a failed live run.
|
|
119
|
+
*
|
|
120
|
+
* `folderLabel` is the caller's original string, used verbatim in the error so
|
|
121
|
+
* the message names what they typed rather than the parsed segments. Set
|
|
122
|
+
* `createMissing` to build absent segments instead of throwing — the whole
|
|
123
|
+
* chain, so a nested destination can be created in one call.
|
|
124
|
+
*/
|
|
125
|
+
function mailScopeScript(ref, folderLabel = '', createMissing = false) {
|
|
126
|
+
const root = `$store.GetDefaultFolder(${ref.rootId})`;
|
|
127
|
+
if (ref.segments.length === 0)
|
|
128
|
+
return `$scope = ${root}\n$scopeCreated = $false`;
|
|
129
|
+
return `
|
|
130
|
+
${exports.FIND_FOLDER_PS}
|
|
131
|
+
$segments = @(${ref.segments.map(s => `'${(0, run_1.psEscape)(s)}'`).join(',')})
|
|
132
|
+
$scopeRoot = ${root}
|
|
133
|
+
$scope = $scopeRoot
|
|
134
|
+
$deepest = $scopeRoot
|
|
135
|
+
$scopeCreated = $false
|
|
136
|
+
foreach ($seg in $segments) {
|
|
137
|
+
$next = $null
|
|
138
|
+
foreach ($f in $scope.Folders) { if ($f.Name -ieq $seg) { $next = $f; break } }
|
|
139
|
+
if ($next -eq $null) { $deepest = $scope; $scope = $null; break }
|
|
140
|
+
$scope = $next
|
|
141
|
+
}
|
|
142
|
+
# A bare name that isn't a direct child still resolves by recursive search, so one
|
|
143
|
+
# folder string keeps working across tools.
|
|
144
|
+
if ($scope -eq $null -and $segments.Count -eq 1) {
|
|
145
|
+
$scope = Find-FolderByName $scopeRoot $segments[0] 3
|
|
146
|
+
}
|
|
147
|
+
if ($scope -eq $null -and ${createMissing ? '$true' : '$false'}) {
|
|
148
|
+
# Rebuild the whole chain from the root, creating only what is genuinely absent.
|
|
149
|
+
$scope = $scopeRoot
|
|
150
|
+
foreach ($seg in $segments) {
|
|
151
|
+
$next = $null
|
|
152
|
+
foreach ($f in $scope.Folders) { if ($f.Name -ieq $seg) { $next = $f; break } }
|
|
153
|
+
if ($next -eq $null) { $next = $scope.Folders.Add($seg); $scopeCreated = $true }
|
|
154
|
+
$scope = $next
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if ($scope -eq $null) {
|
|
158
|
+
$names = @()
|
|
159
|
+
foreach ($f in $deepest.Folders) { $names += $f.Name }
|
|
160
|
+
throw "Folder '${(0, run_1.psEscape)(folderLabel)}' not found. Folders under '$($deepest.Name)': $($names -join ', ')"
|
|
161
|
+
}`;
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=scripts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scripts.js","sourceRoot":"","sources":["../../src/windows/scripts.ts"],"names":[],"mappings":";;;AA+BA,sCASC;AAkBD,4CAUC;AAMD,sCAIC;AAgDD,0CAoCC;AAlKD,0EAA0E;AAC1E,EAAE;AACF,gFAAgF;AAChF,8EAA8E;AAC9E,iCAAiC;AACjC,+BAAiC;AAGjC;;;;;;GAMG;AACH,MAAM,UAAU,GAAG;;;;;CAKlB,CAAC;AAEF;;;;;;;;GAQG;AACH,SAAgB,aAAa,CAAC,YAAoB;IAC9C,OAAO,GAAG,UAAU;aACX,IAAA,cAAQ,EAAC,YAAY,CAAC;;;;;;CAMlC,CAAC;AACF,CAAC;AAED,+EAA+E;AAClE,QAAA,iBAAiB,GAAG;;;CAGhC,CAAC;AAEF;;;;;;;;;GASG;AACH,SAAgB,gBAAgB,CAAC,YAAoB;IACjD,OAAO;;;;;2EAKgE,IAAA,cAAQ,EAAC,YAAY,CAAC;;;CAGhG,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,SAAgB,aAAa,CAAC,OAAe,EAAE,OAAgB;IAC3D,OAAO,OAAO;QACV,CAAC,CAAC,8BAA8B,IAAA,cAAQ,EAAC,OAAO,CAAC,OAAO,IAAA,cAAQ,EAAC,OAAO,CAAC,IAAI;QAC7E,CAAC,CAAC,8BAA8B,IAAA,cAAQ,EAAC,OAAO,CAAC,IAAI,CAAC;AAC9D,CAAC;AAED;;;;;;GAMG;AACU,QAAA,cAAc,GAAG;;;;;;CAM7B,CAAC;AAEF;;;GAGG;AACU,QAAA,cAAc,GAAG;;;;;;;;;;;;CAY7B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,SAAgB,eAAe,CAAC,GAAkB,EAAE,WAAW,GAAG,EAAE,EAAE,aAAa,GAAG,KAAK;IACvF,MAAM,IAAI,GAAG,2BAA2B,GAAG,CAAC,MAAM,GAAG,CAAC;IACtD,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,YAAY,IAAI,0BAA0B,CAAC;IACjF,OAAO;EACT,sBAAc;gBACA,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,IAAA,cAAQ,EAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;eACpD,IAAI;;;;;;;;;;;;;;;4BAeS,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ;;;;;;;;;;;;;qBAazC,IAAA,cAAQ,EAAC,WAAW,CAAC;EACxC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReplyEmailParams, ReplyEmailResult, SendEmailParams } from '../types';
|
|
2
|
+
/** Send (or display for review) an email through Outlook COM. */
|
|
3
|
+
export declare function sendOutlookEmail(params: SendEmailParams): Promise<void>;
|
|
4
|
+
/**
|
|
5
|
+
* Reply to an email, inserting the caller's HTML above the quoted original.
|
|
6
|
+
*
|
|
7
|
+
* The body may be given directly or named as a saved template — see
|
|
8
|
+
* `composeReplyHtml`, which resolves the template, its section and its
|
|
9
|
+
* placeholders before anything reaches Outlook.
|
|
10
|
+
*/
|
|
11
|
+
export declare function replyOutlookEmail(params: ReplyEmailParams): Promise<ReplyEmailResult>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sendOutlookEmail = sendOutlookEmail;
|
|
4
|
+
exports.replyOutlookEmail = replyOutlookEmail;
|
|
5
|
+
// Composing outgoing mail: a new email, and a reply to an existing one.
|
|
6
|
+
const run_1 = require("./run");
|
|
7
|
+
const scripts_1 = require("./scripts");
|
|
8
|
+
const replyBody_1 = require("../shared/replyBody");
|
|
9
|
+
const json_1 = require("../shared/json");
|
|
10
|
+
const signatures_1 = require("./signatures");
|
|
11
|
+
const templates_1 = require("./templates");
|
|
12
|
+
/**
|
|
13
|
+
* Set the sending account through IDispatch reflection.
|
|
14
|
+
*
|
|
15
|
+
* Direct assignment (`$mail.SendUsingAccount = $account`) is a silent no-op under
|
|
16
|
+
* PowerShell's COM binding — the mail then goes out from whichever account
|
|
17
|
+
* Outlook considers default, which is the failure this package exists to prevent.
|
|
18
|
+
*/
|
|
19
|
+
function sendUsingAccount(variable) {
|
|
20
|
+
return `[void]$${variable}.GetType().InvokeMember('SendUsingAccount', [Reflection.BindingFlags]::SetProperty, $null, $${variable}, @($account))`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* What to do with a composed item: send it, show it, or file it silently.
|
|
24
|
+
*
|
|
25
|
+
* `Display()` only shows the compose window — it never files the item, so a
|
|
26
|
+
* windowless draft has to `Save()` into Drafts explicitly.
|
|
27
|
+
*/
|
|
28
|
+
function composeAction(variable, sendImmediately, openDraftWindow) {
|
|
29
|
+
if (sendImmediately)
|
|
30
|
+
return `$${variable}.Send()`;
|
|
31
|
+
return openDraftWindow !== false ? `$${variable}.Display()` : `$${variable}.Save()`;
|
|
32
|
+
}
|
|
33
|
+
/** Send (or display for review) an email through Outlook COM. */
|
|
34
|
+
async function sendOutlookEmail(params) {
|
|
35
|
+
(0, run_1.requireWindows)();
|
|
36
|
+
const attachLine = params.attachmentPath
|
|
37
|
+
? `$mail.Attachments.Add('${(0, run_1.psEscape)(params.attachmentPath)}') | Out-Null`
|
|
38
|
+
: '';
|
|
39
|
+
// The HTML body goes through a file: inline, a large email is what pushes the
|
|
40
|
+
// generated script past the command-line length limit.
|
|
41
|
+
const body = (0, run_1.scriptInput)('email-body', params.htmlBody);
|
|
42
|
+
const script = `${(0, scripts_1.accountScript)(params.emailAccount)}
|
|
43
|
+
$mail = $outlook.CreateItem(0)
|
|
44
|
+
${sendUsingAccount('mail')}
|
|
45
|
+
$mail.To = '${(0, run_1.psEscape)(params.to)}'
|
|
46
|
+
$mail.CC = '${(0, run_1.psEscape)(params.cc || '')}'
|
|
47
|
+
$mail.Subject = '${(0, run_1.psEscape)(params.subject)}'
|
|
48
|
+
$mail.HTMLBody = [IO.File]::ReadAllText('${(0, run_1.psEscape)(body.path)}', [Text.Encoding]::UTF8)
|
|
49
|
+
${attachLine}
|
|
50
|
+
${composeAction('mail', params.sendImmediately, params.openDraftWindow)}
|
|
51
|
+
`;
|
|
52
|
+
try {
|
|
53
|
+
await (0, run_1.runPowerShellFile)(script, 120000);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
body.cleanup();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Reply to an email, inserting the caller's HTML above the quoted original.
|
|
61
|
+
*
|
|
62
|
+
* The body may be given directly or named as a saved template — see
|
|
63
|
+
* `composeReplyHtml`, which resolves the template, its section and its
|
|
64
|
+
* placeholders before anything reaches Outlook.
|
|
65
|
+
*/
|
|
66
|
+
async function replyOutlookEmail(params) {
|
|
67
|
+
(0, run_1.requireWindows)();
|
|
68
|
+
const insertHtml = await (0, replyBody_1.composeReplyHtml)(params, {
|
|
69
|
+
readTemplateEmails: templates_1.readTemplateEmails,
|
|
70
|
+
readOutlookSignatureHtml: signatures_1.readOutlookSignatureHtml,
|
|
71
|
+
listOutlookSignatures: signatures_1.listOutlookSignatures,
|
|
72
|
+
});
|
|
73
|
+
const body = (0, run_1.scriptInput)('reply-insert', insertHtml);
|
|
74
|
+
const replyMethod = params.replyAll ? 'ReplyAll' : 'Reply';
|
|
75
|
+
// Outlook builds the reply as a Word document whose own empty paragraph sits
|
|
76
|
+
// inside WordSection1; inserting straight after that div's opening tag puts
|
|
77
|
+
// the new text where the user's cursor would have been. A reply Outlook did
|
|
78
|
+
// not build that way falls back to just inside <body>.
|
|
79
|
+
const script = `${(0, scripts_1.accountScript)(params.emailAccount)}
|
|
80
|
+
${(0, scripts_1.getItemScript)(params.entryId, params.storeId)}
|
|
81
|
+
$reply = $item.${replyMethod}()
|
|
82
|
+
${sendUsingAccount('reply')}
|
|
83
|
+
|
|
84
|
+
$insertHtml = [IO.File]::ReadAllText('${(0, run_1.psEscape)(body.path)}', [Text.Encoding]::UTF8)
|
|
85
|
+
$keyWord = '${(0, run_1.psEscape)(replyBody_1.WORD_SECTION_ANCHOR)}'
|
|
86
|
+
# Bound to a local first: each $reply.HTMLBody read marshals the WHOLE generated
|
|
87
|
+
# reply across COM, and a Word reply on a long thread runs to hundreds of KB.
|
|
88
|
+
$html = $reply.HTMLBody
|
|
89
|
+
$idx = $html.IndexOf($keyWord)
|
|
90
|
+
if ($idx -ge 0) { $idx += $keyWord.Length } else {
|
|
91
|
+
$m = [regex]::Match($html, '<body[^>]*>')
|
|
92
|
+
$idx = if ($m.Success) { $m.Index + $m.Length } else { 0 }
|
|
93
|
+
}
|
|
94
|
+
$reply.HTMLBody = $html.Insert($idx, $insertHtml)
|
|
95
|
+
$to = [string]$reply.To
|
|
96
|
+
$subject = [string]$reply.Subject
|
|
97
|
+
$sender = ''
|
|
98
|
+
try { $sender = [string]$item.SenderEmailAddress } catch {}
|
|
99
|
+
${composeAction('reply', params.sendImmediately, params.openDraftWindow)}
|
|
100
|
+
ConvertTo-Json @{ to = $to; subject = $subject; repliedToSender = $sender }
|
|
101
|
+
`;
|
|
102
|
+
try {
|
|
103
|
+
const parsed = (0, json_1.parseObject)(await (0, run_1.runPowerShell)(script, 60000));
|
|
104
|
+
return {
|
|
105
|
+
to: (0, json_1.str)(parsed.to),
|
|
106
|
+
subject: (0, json_1.str)(parsed.subject),
|
|
107
|
+
repliedToSender: (0, json_1.str)(parsed.repliedToSender),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
body.cleanup();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=send.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"send.js","sourceRoot":"","sources":["../../src/windows/send.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Names of the user's Outlook signatures (the ".htm" files), sorted. Empty off
|
|
3
|
+
* Windows. Reads from disk synchronously but is declared async to match macOS,
|
|
4
|
+
* which has to ask Outlook itself — one signature for both platforms.
|
|
5
|
+
*/
|
|
6
|
+
export declare function listOutlookSignatures(): Promise<string[]>;
|
|
7
|
+
/**
|
|
8
|
+
* Read a named signature's HTML with its image references rewritten to absolute
|
|
9
|
+
* file: URIs. Outlook stores signature images relative to a "<name>_files"
|
|
10
|
+
* folder; once the refs are absolute, assigning the HTML to a mail body lets
|
|
11
|
+
* Outlook resolve and embed the images on Display/Send. Returns '' if the
|
|
12
|
+
* signature can't be found.
|
|
13
|
+
*/
|
|
14
|
+
export declare function readOutlookSignatureHtml(name: string): Promise<string>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.listOutlookSignatures = listOutlookSignatures;
|
|
7
|
+
exports.readOutlookSignatureHtml = readOutlookSignatureHtml;
|
|
8
|
+
// Outlook stores each signature as "<name>.htm" (plus .rtf/.txt and a
|
|
9
|
+
// "<name>_files" folder for images) under %APPDATA%\Microsoft\Signatures, so
|
|
10
|
+
// both of these read the disk rather than driving COM.
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const url_1 = require("url");
|
|
14
|
+
const SIGNATURES_DIR = process.env.APPDATA
|
|
15
|
+
? path_1.default.join(process.env.APPDATA, 'Microsoft', 'Signatures')
|
|
16
|
+
: '';
|
|
17
|
+
/**
|
|
18
|
+
* Names of the user's Outlook signatures (the ".htm" files), sorted. Empty off
|
|
19
|
+
* Windows. Reads from disk synchronously but is declared async to match macOS,
|
|
20
|
+
* which has to ask Outlook itself — one signature for both platforms.
|
|
21
|
+
*/
|
|
22
|
+
async function listOutlookSignatures() {
|
|
23
|
+
if (process.platform !== 'win32' || !SIGNATURES_DIR)
|
|
24
|
+
return [];
|
|
25
|
+
try {
|
|
26
|
+
return fs_1.default.readdirSync(SIGNATURES_DIR)
|
|
27
|
+
.filter(f => f.toLowerCase().endsWith('.htm'))
|
|
28
|
+
.map(f => f.slice(0, -4))
|
|
29
|
+
.sort((a, b) => a.localeCompare(b));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read a named signature's HTML with its image references rewritten to absolute
|
|
37
|
+
* file: URIs. Outlook stores signature images relative to a "<name>_files"
|
|
38
|
+
* folder; once the refs are absolute, assigning the HTML to a mail body lets
|
|
39
|
+
* Outlook resolve and embed the images on Display/Send. Returns '' if the
|
|
40
|
+
* signature can't be found.
|
|
41
|
+
*/
|
|
42
|
+
async function readOutlookSignatureHtml(name) {
|
|
43
|
+
if (process.platform !== 'win32' || !SIGNATURES_DIR)
|
|
44
|
+
return '';
|
|
45
|
+
// Only accept a bare signature name — never a path — so a crafted name can't
|
|
46
|
+
// escape the Signatures folder.
|
|
47
|
+
const safe = path_1.default.basename(name);
|
|
48
|
+
const file = path_1.default.join(SIGNATURES_DIR, `${safe}.htm`);
|
|
49
|
+
if (!fs_1.default.existsSync(file))
|
|
50
|
+
return '';
|
|
51
|
+
// Classic Outlook signatures are saved as windows-1252, not UTF-8; decode by
|
|
52
|
+
// the charset the file declares so accented text / smart quotes survive.
|
|
53
|
+
const buf = fs_1.default.readFileSync(file);
|
|
54
|
+
const head = buf.toString('latin1', 0, 2048);
|
|
55
|
+
const charset = head.match(/charset=["']?([\w-]+)/i)?.[1] || 'utf-8';
|
|
56
|
+
let html;
|
|
57
|
+
try {
|
|
58
|
+
html = new TextDecoder(charset).decode(buf);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
html = buf.toString('utf-8');
|
|
62
|
+
}
|
|
63
|
+
// pathToFileURL encodes spaces/specials the same way Outlook's relative refs
|
|
64
|
+
// are, so prefixing the (already relative, already-encoded) src keeps a valid URI.
|
|
65
|
+
const dirUri = (0, url_1.pathToFileURL)(SIGNATURES_DIR + path_1.default.sep).href;
|
|
66
|
+
html = html.replace(/(src|background)=(["'])(?!https?:|cid:|data:|file:|mailto:|#)/gi, `$1=$2${dirUri}`);
|
|
67
|
+
return html;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=signatures.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signatures.js","sourceRoot":"","sources":["../../src/windows/signatures.ts"],"names":[],"mappings":";;;;;AAgBA,sDAUC;AASD,4DA0BC;AA7DD,sEAAsE;AACtE,6EAA6E;AAC7E,uDAAuD;AACvD,4CAAoB;AACpB,gDAAwB;AACxB,6BAAoC;AAEpC,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO;IACtC,CAAC,CAAC,cAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,YAAY,CAAC;IAC3D,CAAC,CAAC,EAAE,CAAC;AAET;;;;GAIG;AACI,KAAK,UAAU,qBAAqB;IACvC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,cAAc;QAAE,OAAO,EAAE,CAAC;IAC/D,IAAI,CAAC;QACD,OAAO,YAAE,CAAC,WAAW,CAAC,cAAc,CAAC;aAChC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;aAC7C,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;aACxB,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,EAAE,CAAC;IACd,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,wBAAwB,CAAC,IAAY;IACvD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,cAAc;QAAE,OAAO,EAAE,CAAC;IAC/D,6EAA6E;IAC7E,gCAAgC;IAChC,MAAM,IAAI,GAAG,cAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,IAAI,GAAG,cAAI,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,IAAI,MAAM,CAAC,CAAC;IACtD,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACpC,6EAA6E;IAC7E,yEAAyE;IACzE,MAAM,GAAG,GAAG,YAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,wBAAwB,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC;IACrE,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACD,IAAI,GAAG,IAAI,WAAW,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACL,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACjC,CAAC;IACD,6EAA6E;IAC7E,mFAAmF;IACnF,MAAM,MAAM,GAAG,IAAA,mBAAa,EAAC,cAAc,GAAG,cAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;IAC7D,IAAI,GAAG,IAAI,CAAC,OAAO,CACf,iEAAiE,EACjE,QAAQ,MAAM,EAAE,CACnB,CAAC;IACF,OAAO,IAAI,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SaveTemplateResult, TemplateFolderResult } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Read the template emails saved in a mailbox folder (default "Templates"),
|
|
4
|
+
* returning each item's full HTML body. When the folder doesn't exist, returns
|
|
5
|
+
* folderFound:false plus the mailbox's folder names instead of throwing, so the
|
|
6
|
+
* caller can ask the user whether to create it rather than fail.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readTemplateEmails(emailAccount: string, folderName?: string, limit?: number, includeBody?: boolean, subject?: string): Promise<TemplateFolderResult>;
|
|
9
|
+
/**
|
|
10
|
+
* Save a new template email into a mailbox folder (default "Templates"),
|
|
11
|
+
* creating the folder at the mailbox root if it doesn't exist. The item is a
|
|
12
|
+
* plain unsent mail (subject + HTML body) that can be edited in Outlook.
|
|
13
|
+
* Never overwrites an existing item — it always adds a new one, so check with
|
|
14
|
+
* readTemplateEmails first and only call after the user has agreed to create it.
|
|
15
|
+
*/
|
|
16
|
+
export declare function saveTemplateEmail(emailAccount: string, subject: string, htmlBody: string, folderName?: string): Promise<SaveTemplateResult>;
|
|
17
|
+
/**
|
|
18
|
+
* Open an Outlook compose window pre-filled with `currentHtml`, wait for the
|
|
19
|
+
* user to close it, and return the HTML they saved.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately unbounded: the run lasts as long as the person is editing. Give
|
|
22
|
+
* it an AbortSignal (`bridge.withOptions({ signal })`) if the caller needs a way
|
|
23
|
+
* to give up on them.
|
|
24
|
+
*/
|
|
25
|
+
export declare function editEmailTemplate(label: string, currentHtml: string): Promise<string>;
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readTemplateEmails = readTemplateEmails;
|
|
4
|
+
exports.saveTemplateEmail = saveTemplateEmail;
|
|
5
|
+
exports.editEmailTemplate = editEmailTemplate;
|
|
6
|
+
// Template emails: ordinary mail items kept in a mailbox folder and reused as
|
|
7
|
+
// reply bodies, plus the compose-window editor for one.
|
|
8
|
+
const run_1 = require("./run");
|
|
9
|
+
const scripts_1 = require("./scripts");
|
|
10
|
+
const json_1 = require("../shared/json");
|
|
11
|
+
const outlookTemplateSections_1 = require("../outlookTemplateSections");
|
|
12
|
+
const mail_1 = require("../mail");
|
|
13
|
+
const errors_1 = require("../errors");
|
|
14
|
+
const runtime_1 = require("../runtime");
|
|
15
|
+
/**
|
|
16
|
+
* Read the template emails saved in a mailbox folder (default "Templates"),
|
|
17
|
+
* returning each item's full HTML body. When the folder doesn't exist, returns
|
|
18
|
+
* folderFound:false plus the mailbox's folder names instead of throwing, so the
|
|
19
|
+
* caller can ask the user whether to create it rather than fail.
|
|
20
|
+
*/
|
|
21
|
+
async function readTemplateEmails(emailAccount, folderName = 'Templates', limit = 20, includeBody = true, subject = '') {
|
|
22
|
+
(0, run_1.requireWindows)();
|
|
23
|
+
const cap = (0, mail_1.clamp)(limit, 1, 50);
|
|
24
|
+
const wanted = (subject || '').trim();
|
|
25
|
+
const script = `${(0, scripts_1.accountScript)(emailAccount)}
|
|
26
|
+
$root = $account.DeliveryStore.GetRootFolder()
|
|
27
|
+
${scripts_1.FIND_FOLDER_PS}
|
|
28
|
+
$folder = Find-FolderByName $root '${(0, run_1.psEscape)(folderName)}' 3
|
|
29
|
+
if ($folder -eq $null) {
|
|
30
|
+
$names = @()
|
|
31
|
+
foreach ($f in $root.Folders) { $names += $f.Name }
|
|
32
|
+
ConvertTo-Json @{ folderFound = $false; folderPath = ''; templates = @(); availableFolders = $names } -Depth 3
|
|
33
|
+
exit 0
|
|
34
|
+
}
|
|
35
|
+
$includeBody = ${(0, run_1.psBool)(includeBody)}
|
|
36
|
+
$wanted = '${(0, run_1.psEscape)(wanted)}'
|
|
37
|
+
$items = $folder.Items
|
|
38
|
+
$items.Sort('[LastModificationTime]', $true)
|
|
39
|
+
$results = @()
|
|
40
|
+
# Without a subject filter the sort order does the capping (newest first). With one,
|
|
41
|
+
# scan the whole folder — the wanted template need not be among the newest — and stop
|
|
42
|
+
# as soon as the cap is filled.
|
|
43
|
+
$count = $items.Count
|
|
44
|
+
for ($i = 1; $i -le $count; $i++) {
|
|
45
|
+
if ($results.Count -ge ${cap}) { break }
|
|
46
|
+
$item = $null
|
|
47
|
+
try { $item = $items.Item($i) } catch { continue }
|
|
48
|
+
if ($item -eq $null) { continue }
|
|
49
|
+
$cls = 0
|
|
50
|
+
try { $cls = [int]$item.Class } catch {}
|
|
51
|
+
if ($cls -ne 43) { continue } # olMail only
|
|
52
|
+
if ($wanted -ne '') {
|
|
53
|
+
$subj = ''
|
|
54
|
+
try { $subj = [string]$item.Subject } catch {}
|
|
55
|
+
if ($subj -eq $null) { $subj = '' }
|
|
56
|
+
if ($subj.Trim() -ine $wanted) { continue }
|
|
57
|
+
}
|
|
58
|
+
$html = ''
|
|
59
|
+
$preview = ''
|
|
60
|
+
$markers = ''
|
|
61
|
+
$plain = ''
|
|
62
|
+
if ($includeBody) {
|
|
63
|
+
try { $html = [string]$item.HTMLBody } catch {}
|
|
64
|
+
# Embedded (cid:) images live as attachments on the TEMPLATE item: reusing this HTML
|
|
65
|
+
# on a new email shows a broken "linked image" placeholder, and rewriting to file:
|
|
66
|
+
# URIs renders invisibly in modern Outlook. Neither works, so strip the img tags —
|
|
67
|
+
# template emails are text/HTML only.
|
|
68
|
+
$html = [regex]::Replace($html, '<img[^>]*src="cid:[^"]*"[^>]*>', '')
|
|
69
|
+
if ($html.Length -gt 100000) { $html = $html.Substring(0, 100000) }
|
|
70
|
+
} else {
|
|
71
|
+
# Bodies omitted: return a short plain-text preview so the caller can tell templates
|
|
72
|
+
# apart by subject without pulling the full (often huge) Word-generated HTML.
|
|
73
|
+
try { $plain = [string]$item.Body } catch { $plain = '' }
|
|
74
|
+
if ($plain) {
|
|
75
|
+
# ...plus the [[SECTION]] / {{PLACEHOLDER}} markers, scanned from the full text:
|
|
76
|
+
# that's what lets a caller confirm a sectioned template is intact without
|
|
77
|
+
# fetching its body. Word can split a marker across tags in the HTML but not
|
|
78
|
+
# in the plain-text body, so a literal scan is right here.
|
|
79
|
+
# (Backslashes in the regex are doubled because this whole script is a JS
|
|
80
|
+
# template literal — a single one would be eaten before PowerShell saw it.)
|
|
81
|
+
$found = @()
|
|
82
|
+
foreach ($x in [regex]::Matches($plain, '\\[\\[\\s*/?\\s*[A-Za-z0-9_-]{1,40}\\s*\\]\\]|\\{\\{\\s*[A-Za-z0-9_-]{1,40}\\s*\\}\\}')) {
|
|
83
|
+
$t = ($x.Value -replace '\\s', '')
|
|
84
|
+
if ($found -notcontains $t) { $found += $t }
|
|
85
|
+
}
|
|
86
|
+
$markers = ($found -join ' ')
|
|
87
|
+
$preview = $plain.Trim()
|
|
88
|
+
if ($preview.Length -gt 200) { $preview = $preview.Substring(0, 200) }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
$results += [PSCustomObject]@{
|
|
92
|
+
entryId = $item.EntryID
|
|
93
|
+
subject = if ($item.Subject) { $item.Subject.Trim() } else { '' }
|
|
94
|
+
htmlBody = $html
|
|
95
|
+
bodyPreview = $preview
|
|
96
|
+
markers = $markers
|
|
97
|
+
lastModified = $item.LastModificationTime.ToString('yyyy-MM-dd HH:mm')
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
ConvertTo-Json @{ folderFound = $true; folderPath = $folder.FolderPath; templates = $results; availableFolders = @() } -Depth 4
|
|
101
|
+
`;
|
|
102
|
+
const parsed = (0, json_1.parseObject)(await (0, run_1.runPowerShell)(script, 60000));
|
|
103
|
+
return {
|
|
104
|
+
folderFound: Boolean(parsed.folderFound),
|
|
105
|
+
folderPath: (0, json_1.str)(parsed.folderPath),
|
|
106
|
+
templates: (0, json_1.toArray)(parsed.templates).map(item => {
|
|
107
|
+
const e = (0, json_1.record)(item);
|
|
108
|
+
const html = (0, json_1.str)(e.htmlBody);
|
|
109
|
+
// With a body in hand the HTML is authoritative (it survives Word splitting a
|
|
110
|
+
// marker across tags); without one, fall back to what the script scanned out
|
|
111
|
+
// of the plain-text body.
|
|
112
|
+
const markers = (0, outlookTemplateSections_1.findTemplateMarkers)(html || (0, json_1.str)(e.markers));
|
|
113
|
+
return {
|
|
114
|
+
entryId: (0, json_1.str)(e.entryId),
|
|
115
|
+
subject: (0, json_1.str)(e.subject),
|
|
116
|
+
htmlBody: html,
|
|
117
|
+
bodyPreview: (0, json_1.str)(e.bodyPreview),
|
|
118
|
+
sections: markers.sections,
|
|
119
|
+
placeholders: markers.placeholders,
|
|
120
|
+
lastModified: (0, json_1.str)(e.lastModified),
|
|
121
|
+
};
|
|
122
|
+
}),
|
|
123
|
+
availableFolders: (0, json_1.toArray)(parsed.availableFolders).map(json_1.str),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Save a new template email into a mailbox folder (default "Templates"),
|
|
128
|
+
* creating the folder at the mailbox root if it doesn't exist. The item is a
|
|
129
|
+
* plain unsent mail (subject + HTML body) that can be edited in Outlook.
|
|
130
|
+
* Never overwrites an existing item — it always adds a new one, so check with
|
|
131
|
+
* readTemplateEmails first and only call after the user has agreed to create it.
|
|
132
|
+
*/
|
|
133
|
+
async function saveTemplateEmail(emailAccount, subject, htmlBody, folderName = 'Templates') {
|
|
134
|
+
(0, run_1.requireWindows)();
|
|
135
|
+
const body = (0, run_1.scriptInput)('template-body', htmlBody);
|
|
136
|
+
const script = `${(0, scripts_1.accountScript)(emailAccount)}
|
|
137
|
+
$root = $account.DeliveryStore.GetRootFolder()
|
|
138
|
+
${scripts_1.FIND_FOLDER_PS}
|
|
139
|
+
$folder = Find-FolderByName $root '${(0, run_1.psEscape)(folderName)}' 3
|
|
140
|
+
$folderCreated = $false
|
|
141
|
+
if ($folder -eq $null) {
|
|
142
|
+
$folder = $root.Folders.Add('${(0, run_1.psEscape)(folderName)}')
|
|
143
|
+
$folderCreated = $true
|
|
144
|
+
}
|
|
145
|
+
$mail = $outlook.CreateItem(0)
|
|
146
|
+
$mail.Subject = '${(0, run_1.psEscape)(subject)}'
|
|
147
|
+
$mail.HTMLBody = [IO.File]::ReadAllText('${(0, run_1.psEscape)(body.path)}', [Text.Encoding]::UTF8)
|
|
148
|
+
$mail.Save()
|
|
149
|
+
$moved = $mail.Move($folder)
|
|
150
|
+
ConvertTo-Json @{ folderPath = $folder.FolderPath; folderCreated = $folderCreated }
|
|
151
|
+
`;
|
|
152
|
+
try {
|
|
153
|
+
const parsed = (0, json_1.parseObject)(await (0, run_1.runPowerShell)(script, 60000));
|
|
154
|
+
return {
|
|
155
|
+
folderPath: (0, json_1.str)(parsed.folderPath),
|
|
156
|
+
folderCreated: Boolean(parsed.folderCreated),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
body.cleanup();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Open an Outlook compose window pre-filled with `currentHtml`, wait for the
|
|
165
|
+
* user to close it, and return the HTML they saved.
|
|
166
|
+
*
|
|
167
|
+
* Deliberately unbounded: the run lasts as long as the person is editing. Give
|
|
168
|
+
* it an AbortSignal (`bridge.withOptions({ signal })`) if the caller needs a way
|
|
169
|
+
* to give up on them.
|
|
170
|
+
*/
|
|
171
|
+
async function editEmailTemplate(label, currentHtml) {
|
|
172
|
+
(0, run_1.requireWindows)();
|
|
173
|
+
const input = (0, run_1.scriptInput)('template-input', currentHtml);
|
|
174
|
+
const outputFile = (0, runtime_1.tempFile)('template-output', 'html');
|
|
175
|
+
const subject = `${label} - Save (Ctrl+S) and close when done`;
|
|
176
|
+
const script = `
|
|
177
|
+
$ErrorActionPreference = 'Stop'
|
|
178
|
+
$inputPath = '${(0, run_1.psEscape)(input.path)}'
|
|
179
|
+
$outputPath = '${(0, run_1.psEscape)(outputFile)}'
|
|
180
|
+
$subject = '${(0, run_1.psEscape)(subject)}'
|
|
181
|
+
|
|
182
|
+
$outlook = New-Object -ComObject Outlook.Application
|
|
183
|
+
$ns = $outlook.GetNamespace('mapi')
|
|
184
|
+
$mail = $outlook.CreateItem(0)
|
|
185
|
+
$mail.Subject = $subject
|
|
186
|
+
$mail.HTMLBody = [IO.File]::ReadAllText($inputPath, [Text.Encoding]::UTF8)
|
|
187
|
+
$mail.Display()
|
|
188
|
+
|
|
189
|
+
# Give the inspector time to fully initialize
|
|
190
|
+
Start-Sleep -Seconds 2
|
|
191
|
+
|
|
192
|
+
# Poll until the compose window is closed, keeping the last live body as a
|
|
193
|
+
# fallback for the case where the user edits but never presses Ctrl+S.
|
|
194
|
+
$liveBody = ''
|
|
195
|
+
while ($true) {
|
|
196
|
+
Start-Sleep -Milliseconds 700
|
|
197
|
+
$open = $false
|
|
198
|
+
try {
|
|
199
|
+
foreach ($insp in $outlook.Inspectors) {
|
|
200
|
+
$ci = $null
|
|
201
|
+
try { $ci = $insp.CurrentItem } catch {}
|
|
202
|
+
if ($ci -ne $null -and $ci.Subject -eq $subject) {
|
|
203
|
+
$open = $true
|
|
204
|
+
try { $liveBody = $ci.HTMLBody } catch {}
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch {}
|
|
209
|
+
if (-not $open) { break }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# Prefer the saved draft — it reflects the user's Ctrl+S and survives the
|
|
213
|
+
# inspector closing, unlike the original mail reference. Also removes any
|
|
214
|
+
# leftover editor drafts so they don't pile up in the Drafts folder.
|
|
215
|
+
$savedBody = ''
|
|
216
|
+
try {
|
|
217
|
+
$drafts = $ns.GetDefaultFolder(16)
|
|
218
|
+
$draftItems = $drafts.Items
|
|
219
|
+
for ($i = $draftItems.Count; $i -ge 1; $i--) {
|
|
220
|
+
$it = $draftItems.Item($i)
|
|
221
|
+
if ($it.Subject -eq $subject) {
|
|
222
|
+
if (-not $savedBody) { try { $savedBody = $it.HTMLBody } catch {} }
|
|
223
|
+
try { $it.Delete() } catch {}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch {}
|
|
227
|
+
|
|
228
|
+
$finalBody = if ($savedBody) { $savedBody } else { $liveBody }
|
|
229
|
+
if ($finalBody) {
|
|
230
|
+
[IO.File]::WriteAllText($outputPath, $finalBody, [Text.Encoding]::UTF8)
|
|
231
|
+
}
|
|
232
|
+
`;
|
|
233
|
+
try {
|
|
234
|
+
// 0 disables the timeout: this run is as long as the edit takes.
|
|
235
|
+
await (0, run_1.runPowerShellFile)(script, 0);
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
input.cleanup();
|
|
239
|
+
}
|
|
240
|
+
const result = (0, run_1.readScriptOutput)(outputFile);
|
|
241
|
+
if (!result) {
|
|
242
|
+
throw new errors_1.NotFoundError('template', 'No template saved. Did you save (Ctrl+S) before closing?');
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=templates.js.map
|