@dawnlit/outlook-bridge 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +150 -183
  2. package/dist/backend.d.ts +151 -0
  3. package/dist/backend.js +3 -0
  4. package/dist/backend.js.map +1 -0
  5. package/dist/bridge.d.ts +56 -0
  6. package/dist/bridge.js +507 -0
  7. package/dist/bridge.js.map +1 -0
  8. package/dist/errors.d.ts +81 -43
  9. package/dist/errors.js +98 -52
  10. package/dist/errors.js.map +1 -1
  11. package/dist/index.d.ts +10 -10
  12. package/dist/index.js +25 -52
  13. package/dist/index.js.map +1 -1
  14. package/dist/mac/accounts.d.ts +7 -2
  15. package/dist/mac/accounts.js +23 -6
  16. package/dist/mac/accounts.js.map +1 -1
  17. package/dist/mac/attachments.d.ts +5 -25
  18. package/dist/mac/attachments.js +40 -99
  19. package/dist/mac/attachments.js.map +1 -1
  20. package/dist/mac/bounces.d.ts +12 -27
  21. package/dist/mac/bounces.js +95 -112
  22. package/dist/mac/bounces.js.map +1 -1
  23. package/dist/mac/cleanup.d.ts +11 -25
  24. package/dist/mac/cleanup.js +50 -76
  25. package/dist/mac/cleanup.js.map +1 -1
  26. package/dist/mac/drafts.d.ts +13 -34
  27. package/dist/mac/drafts.js +95 -169
  28. package/dist/mac/drafts.js.map +1 -1
  29. package/dist/mac/folders.d.ts +10 -21
  30. package/dist/mac/folders.js +33 -47
  31. package/dist/mac/folders.js.map +1 -1
  32. package/dist/mac/index.d.ts +2 -22
  33. package/dist/mac/index.js +19 -81
  34. package/dist/mac/index.js.map +1 -1
  35. package/dist/mac/profile.d.ts +13 -0
  36. package/dist/mac/profile.js +149 -0
  37. package/dist/mac/profile.js.map +1 -0
  38. package/dist/mac/read.d.ts +18 -36
  39. package/dist/mac/read.js +154 -208
  40. package/dist/mac/read.js.map +1 -1
  41. package/dist/mac/run.d.ts +35 -39
  42. package/dist/mac/run.js +97 -102
  43. package/dist/mac/run.js.map +1 -1
  44. package/dist/mac/scripts.d.ts +78 -86
  45. package/dist/mac/scripts.js +125 -115
  46. package/dist/mac/scripts.js.map +1 -1
  47. package/dist/mac/send.d.ts +10 -20
  48. package/dist/mac/send.js +64 -86
  49. package/dist/mac/send.js.map +1 -1
  50. package/dist/mac/signatures.d.ts +2 -2
  51. package/dist/mac/signatures.js +9 -11
  52. package/dist/mac/signatures.js.map +1 -1
  53. package/dist/mac/templates.d.ts +14 -26
  54. package/dist/mac/templates.js +75 -113
  55. package/dist/mac/templates.js.map +1 -1
  56. package/dist/mail.d.ts +77 -45
  57. package/dist/mail.js +166 -73
  58. package/dist/mail.js.map +1 -1
  59. package/dist/runtime.d.ts +76 -43
  60. package/dist/runtime.js +103 -48
  61. package/dist/runtime.js.map +1 -1
  62. package/dist/shared/args.d.ts +48 -0
  63. package/dist/shared/args.js +216 -0
  64. package/dist/shared/args.js.map +1 -0
  65. package/dist/shared/attachmentMatch.d.ts +28 -16
  66. package/dist/shared/attachmentMatch.js +60 -22
  67. package/dist/shared/attachmentMatch.js.map +1 -1
  68. package/dist/shared/bounceReport.d.ts +11 -0
  69. package/dist/shared/bounceReport.js +45 -0
  70. package/dist/shared/bounceReport.js.map +1 -0
  71. package/dist/shared/bounceRules.d.ts +38 -10
  72. package/dist/shared/bounceRules.js +50 -32
  73. package/dist/shared/bounceRules.js.map +1 -1
  74. package/dist/shared/exec.d.ts +21 -0
  75. package/dist/shared/exec.js +47 -0
  76. package/dist/shared/exec.js.map +1 -0
  77. package/dist/shared/json.d.ts +12 -6
  78. package/dist/shared/json.js +29 -23
  79. package/dist/shared/json.js.map +1 -1
  80. package/dist/shared/locate.d.ts +39 -0
  81. package/dist/shared/locate.js +129 -0
  82. package/dist/shared/locate.js.map +1 -0
  83. package/dist/shared/replyBody.d.ts +42 -32
  84. package/dist/shared/replyBody.js +84 -71
  85. package/dist/shared/replyBody.js.map +1 -1
  86. package/dist/templateBody.d.ts +63 -0
  87. package/dist/{outlookTemplateSections.js → templateBody.js} +72 -72
  88. package/dist/templateBody.js.map +1 -0
  89. package/dist/types.d.ts +360 -165
  90. package/dist/types.js +8 -9
  91. package/dist/types.js.map +1 -1
  92. package/dist/windows/accounts.d.ts +10 -1
  93. package/dist/windows/accounts.js +25 -13
  94. package/dist/windows/accounts.js.map +1 -1
  95. package/dist/windows/attachments.d.ts +9 -24
  96. package/dist/windows/attachments.js +94 -119
  97. package/dist/windows/attachments.js.map +1 -1
  98. package/dist/windows/bounces.d.ts +13 -26
  99. package/dist/windows/bounces.js +169 -200
  100. package/dist/windows/bounces.js.map +1 -1
  101. package/dist/windows/cleanup.d.ts +19 -23
  102. package/dist/windows/cleanup.js +93 -111
  103. package/dist/windows/cleanup.js.map +1 -1
  104. package/dist/windows/drafts.d.ts +16 -35
  105. package/dist/windows/drafts.js +167 -253
  106. package/dist/windows/drafts.js.map +1 -1
  107. package/dist/windows/folders.d.ts +8 -13
  108. package/dist/windows/folders.js +45 -52
  109. package/dist/windows/folders.js.map +1 -1
  110. package/dist/windows/index.d.ts +2 -27
  111. package/dist/windows/index.js +11 -65
  112. package/dist/windows/index.js.map +1 -1
  113. package/dist/windows/read.d.ts +24 -45
  114. package/dist/windows/read.js +266 -279
  115. package/dist/windows/read.js.map +1 -1
  116. package/dist/windows/run.d.ts +39 -39
  117. package/dist/windows/run.js +151 -111
  118. package/dist/windows/run.js.map +1 -1
  119. package/dist/windows/scripts.d.ts +71 -38
  120. package/dist/windows/scripts.js +123 -71
  121. package/dist/windows/scripts.js.map +1 -1
  122. package/dist/windows/send.d.ts +6 -11
  123. package/dist/windows/send.js +70 -79
  124. package/dist/windows/send.js.map +1 -1
  125. package/dist/windows/signatures.d.ts +4 -10
  126. package/dist/windows/signatures.js +30 -37
  127. package/dist/windows/signatures.js.map +1 -1
  128. package/dist/windows/templates.d.ts +12 -18
  129. package/dist/windows/templates.js +126 -151
  130. package/dist/windows/templates.js.map +1 -1
  131. package/package.json +5 -4
  132. package/dist/OutlookService.d.ts +0 -43
  133. package/dist/OutlookService.js +0 -137
  134. package/dist/OutlookService.js.map +0 -1
  135. package/dist/outlookTemplateSections.d.ts +0 -62
  136. package/dist/outlookTemplateSections.js.map +0 -1
  137. package/dist/shared/capabilities.d.ts +0 -3
  138. package/dist/shared/capabilities.js +0 -9
  139. package/dist/shared/capabilities.js.map +0 -1
@@ -4,243 +4,218 @@ exports.readTemplateEmails = readTemplateEmails;
4
4
  exports.saveTemplateEmail = saveTemplateEmail;
5
5
  exports.editEmailTemplate = editEmailTemplate;
6
6
  // Template emails: ordinary mail items kept in a mailbox folder and reused as
7
- // reply bodies, plus the compose-window editor for one.
7
+ // email bodies, plus the compose-window editor for one.
8
8
  const run_1 = require("./run");
9
9
  const scripts_1 = require("./scripts");
10
10
  const json_1 = require("../shared/json");
11
- const outlookTemplateSections_1 = require("../outlookTemplateSections");
11
+ const templateBody_1 = require("../templateBody");
12
12
  const mail_1 = require("../mail");
13
13
  const errors_1 = require("../errors");
14
14
  const runtime_1 = require("../runtime");
15
+ /** How deep under the mailbox root a template folder is searched for. */
16
+ const TEMPLATE_SEARCH_DEPTH = 3;
17
+ /** Cap on one template body; a Word-generated one is large, and a runaway one shouldn't sink the call. */
18
+ const MAX_TEMPLATE_HTML = 100_000;
19
+ /** Plain text standing in for a body that wasn't asked for. */
20
+ const PREVIEW_CHARS = 200;
15
21
  /**
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.
22
+ * The template emails in a mailbox folder, newest first. A missing folder is
23
+ * reported as folderFound:false with the mailbox's top-level folder names
24
+ * rather than thrown, so a caller can offer to create it.
20
25
  */
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()
26
+ async function readTemplateEmails(request) {
27
+ const output = await (0, run_1.runPowerShellJson)(`${(0, scripts_1.accountScript)(request.account)}
28
+ ${scripts_1.DELIVERY_STORE_PS}
29
+ $root = $store.GetRootFolder()
27
30
  ${scripts_1.FIND_FOLDER_PS}
28
- $folder = Find-FolderByName $root '${(0, run_1.psEscape)(folderName)}' 3
31
+ $folder = Find-FolderByName $root ${(0, run_1.psString)(request.folder)} ${(0, run_1.psInt)(TEMPLATE_SEARCH_DEPTH)}
29
32
  if ($folder -eq $null) {
30
33
  $names = @()
31
- foreach ($f in $root.Folders) { $names += $f.Name }
32
- ConvertTo-Json @{ folderFound = $false; folderPath = ''; templates = @(); availableFolders = $names } -Depth 3
34
+ foreach ($f in $root.Folders) { $names += [string]$f.Name }
35
+ ConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{ folderFound = $false; folderPath = ''; templates = @(); availableFolders = @($names) })
33
36
  exit 0
34
37
  }
35
- $includeBody = ${(0, run_1.psBool)(includeBody)}
36
- $wanted = '${(0, run_1.psEscape)(wanted)}'
38
+ $includeBody = ${(0, run_1.psBool)(request.includeBody)}
39
+ $wanted = ${(0, run_1.psString)(request.subject ?? '')}
40
+ $limit = ${(0, run_1.psInt)(request.limit)}
41
+ $maxHtml = ${(0, run_1.psInt)(MAX_TEMPLATE_HTML)}
42
+ $previewChars = ${(0, run_1.psInt)(PREVIEW_CHARS)}
37
43
  $items = $folder.Items
38
44
  $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.
45
+ $rows = @()
43
46
  $count = $items.Count
44
- for ($i = 1; $i -le $count; $i++) {
45
- if ($results.Count -ge ${cap}) { break }
47
+ # Without a subject the sort does the capping, newest first. With one, the whole
48
+ # folder is scanned — the template wanted need not be among the newest.
49
+ for ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {
46
50
  $item = $null
47
51
  try { $item = $items.Item($i) } catch { continue }
48
52
  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
- }
53
+ $class = 0
54
+ try { $class = [int]$item.Class } catch {}
55
+ if ($class -ne 43) { continue }
56
+ $subject = ''
57
+ try { $subject = ([string]$item.Subject).Trim() } catch {}
58
+ if ($wanted -and $subject -ine $wanted) { continue }
58
59
  $html = ''
59
60
  $preview = ''
60
61
  $markers = ''
61
- $plain = ''
62
62
  if ($includeBody) {
63
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.
64
+ # Embedded (cid:) images live as attachments on the TEMPLATE item, so its
65
+ # HTML reused elsewhere shows broken-image placeholders. Template emails
66
+ # are text and markup only.
68
67
  $html = [regex]::Replace($html, '<img[^>]*src="cid:[^"]*"[^>]*>', '')
69
- if ($html.Length -gt 100000) { $html = $html.Substring(0, 100000) }
68
+ if ($html.Length -gt $maxHtml) { $html = $html.Substring(0, $maxHtml) }
70
69
  } 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) }
70
+ # No body asked for: a short preview, plus the section and placeholder
71
+ # markers scanned here from the full plain text — so a caller can confirm
72
+ # a template is intact without its body crossing stdout. Word can split a
73
+ # marker across tags in the HTML but not in the plain text.
74
+ $plain = ''
75
+ try { $plain = [string]$item.Body } catch {}
76
+ $found = @()
77
+ foreach ($m in [regex]::Matches($plain, '\\[\\[\\s*/?\\s*[A-Za-z0-9_-]{1,40}\\s*\\]\\]|\\{\\{\\s*[A-Za-z0-9_-]{1,40}\\s*\\}\\}')) {
78
+ $token = $m.Value -replace '\\s', ''
79
+ if ($found -notcontains $token) { $found += $token }
89
80
  }
81
+ $markers = $found -join ' '
82
+ $preview = $plain.Trim()
83
+ if ($preview.Length -gt $previewChars) { $preview = $preview.Substring(0, $previewChars) }
90
84
  }
91
- $results += [PSCustomObject]@{
92
- entryId = $item.EntryID
93
- subject = if ($item.Subject) { $item.Subject.Trim() } else { '' }
85
+ $modified = ''
86
+ try { $modified = $item.LastModificationTime.ToString('yyyy-MM-dd HH:mm') } catch {}
87
+ $rows += [PSCustomObject]@{
88
+ entryId = [string]$item.EntryID
89
+ subject = $subject
94
90
  htmlBody = $html
95
91
  bodyPreview = $preview
96
92
  markers = $markers
97
- lastModified = $item.LastModificationTime.ToString('yyyy-MM-dd HH:mm')
93
+ lastModified = $modified
98
94
  }
99
95
  }
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));
96
+ ConvertTo-Json -Depth 4 -InputObject ([PSCustomObject]@{
97
+ folderFound = $true
98
+ folderPath = [string]$folder.FolderPath
99
+ templates = @($rows)
100
+ availableFolders = @()
101
+ })
102
+ `, 'standard');
103
+ const e = (0, json_1.record)(output);
103
104
  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));
105
+ folderFound: (0, json_1.bool)(e.folderFound),
106
+ folderPath: (0, json_1.str)(e.folderPath),
107
+ templates: (0, json_1.toArray)(e.templates).map(row => {
108
+ const t = (0, json_1.record)(row);
109
+ const html = (0, json_1.str)(t.htmlBody);
110
+ // With the HTML in hand it is authoritative (markers survive Word
111
+ // splitting them across tags); without it, the markers the script
112
+ // scanned out of the plain text stand in.
113
+ const markers = (0, templateBody_1.findTemplateMarkers)(request.includeBody ? html : (0, json_1.str)(t.markers));
113
114
  return {
114
- entryId: (0, json_1.str)(e.entryId),
115
- subject: (0, json_1.str)(e.subject),
115
+ entryId: (0, json_1.str)(t.entryId),
116
+ subject: (0, json_1.str)(t.subject),
116
117
  htmlBody: html,
117
- bodyPreview: (0, json_1.str)(e.bodyPreview),
118
+ bodyPreview: (0, json_1.str)(t.bodyPreview),
118
119
  sections: markers.sections,
119
120
  placeholders: markers.placeholders,
120
- lastModified: (0, json_1.str)(e.lastModified),
121
+ lastModified: (0, json_1.str)(t.lastModified),
121
122
  };
122
123
  }),
123
- availableFolders: (0, json_1.toArray)(parsed.availableFolders).map(json_1.str),
124
+ availableFolders: (0, json_1.toArray)(e.availableFolders).map(json_1.str),
124
125
  };
125
126
  }
126
127
  /**
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.
128
+ * Save a new template email into a folder, creating the folder at the mailbox
129
+ * root when absent. Always adds a new item — never overwrites one.
132
130
  */
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()
131
+ async function saveTemplateEmail(request) {
132
+ const body = (0, run_1.scriptInput)('template-body', request.htmlBody);
133
+ try {
134
+ const output = await (0, run_1.runPowerShellJson)(`${(0, scripts_1.accountScript)(request.account)}
135
+ ${scripts_1.DELIVERY_STORE_PS}
136
+ $root = $store.GetRootFolder()
138
137
  ${scripts_1.FIND_FOLDER_PS}
139
- $folder = Find-FolderByName $root '${(0, run_1.psEscape)(folderName)}' 3
138
+ $folderName = ${(0, run_1.psString)(request.folder)}
139
+ $folder = Find-FolderByName $root $folderName ${(0, run_1.psInt)(TEMPLATE_SEARCH_DEPTH)}
140
140
  $folderCreated = $false
141
141
  if ($folder -eq $null) {
142
- $folder = $root.Folders.Add('${(0, run_1.psEscape)(folderName)}')
142
+ $folder = $root.Folders.Add($folderName)
143
143
  $folderCreated = $true
144
144
  }
145
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)
146
+ $mail.Subject = ${(0, run_1.psString)(request.subject)}
147
+ $mail.HTMLBody = [IO.File]::ReadAllText(${(0, run_1.psString)(body.path)}, [Text.Encoding]::UTF8)
148
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
- };
149
+ [void]$mail.Move($folder)
150
+ ConvertTo-Json -Compress -InputObject ([PSCustomObject]@{ folderPath = [string]$folder.FolderPath; folderCreated = $folderCreated })
151
+ `, 'standard');
152
+ const e = (0, json_1.record)(output);
153
+ return { folderPath: (0, json_1.str)(e.folderPath), folderCreated: (0, json_1.bool)(e.folderCreated) };
158
154
  }
159
155
  finally {
160
156
  body.cleanup();
161
157
  }
162
158
  }
163
159
  /**
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.
160
+ * Open a compose window pre-filled with the HTML, wait for it to close, and
161
+ * return what was saved (Ctrl+S) — or, failing a save, the body as it last
162
+ * stood while the window was open. Leftover editor drafts are removed.
170
163
  */
171
- async function editEmailTemplate(label, currentHtml) {
172
- (0, run_1.requireWindows)();
173
- const input = (0, run_1.scriptInput)('template-input', currentHtml);
164
+ async function editEmailTemplate(request) {
165
+ const input = (0, run_1.scriptInput)('template-input', request.html);
174
166
  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')
167
+ try {
168
+ await (0, run_1.runPowerShell)(`${scripts_1.SESSION_PS}
169
+ $subject = ${(0, run_1.psString)(`${request.label} - Save (Ctrl+S) and close when done`)}
170
+ $outputPath = ${(0, run_1.psString)(outputFile)}
184
171
  $mail = $outlook.CreateItem(0)
185
172
  $mail.Subject = $subject
186
- $mail.HTMLBody = [IO.File]::ReadAllText($inputPath, [Text.Encoding]::UTF8)
173
+ $mail.HTMLBody = [IO.File]::ReadAllText(${(0, run_1.psString)(input.path)}, [Text.Encoding]::UTF8)
187
174
  $mail.Display()
188
-
189
- # Give the inspector time to fully initialize
190
175
  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.
176
+ # Poll until the compose window closes, keeping the last live body as the
177
+ # fallback for an edit that was never saved.
194
178
  $liveBody = ''
195
179
  while ($true) {
196
180
  Start-Sleep -Milliseconds 700
197
181
  $open = $false
198
182
  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) {
183
+ foreach ($inspector in $outlook.Inspectors) {
184
+ $current = $null
185
+ try { $current = $inspector.CurrentItem } catch {}
186
+ if ($current -ne $null -and [string]$current.Subject -eq $subject) {
203
187
  $open = $true
204
- try { $liveBody = $ci.HTMLBody } catch {}
188
+ try { $liveBody = [string]$current.HTMLBody } catch {}
205
189
  break
206
190
  }
207
191
  }
208
192
  } catch {}
209
193
  if (-not $open) { break }
210
194
  }
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.
195
+ # Prefer the saved draft: it is what the person chose to keep, and it outlives
196
+ # the window. Every editor draft is removed so none pile up in Drafts.
215
197
  $savedBody = ''
216
198
  try {
217
- $drafts = $ns.GetDefaultFolder(16)
218
- $draftItems = $drafts.Items
199
+ $draftItems = $ns.GetDefaultFolder(${(0, run_1.psInt)(mail_1.FolderId.Drafts)}).Items
219
200
  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 {}
201
+ $draft = $draftItems.Item($i)
202
+ if ([string]$draft.Subject -eq $subject) {
203
+ if (-not $savedBody) { try { $savedBody = [string]$draft.HTMLBody } catch {} }
204
+ try { $draft.Delete() } catch {}
224
205
  }
225
206
  }
226
207
  } catch {}
227
-
228
208
  $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);
209
+ if ($finalBody) { [IO.File]::WriteAllText($outputPath, $finalBody, [Text.Encoding]::UTF8) }
210
+ `, 'interactive');
236
211
  }
237
212
  finally {
238
213
  input.cleanup();
239
214
  }
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?');
215
+ const saved = (0, run_1.readScriptOutput)(outputFile);
216
+ if (!saved) {
217
+ throw new errors_1.NotFoundError('template', 'No template was saved. Save (Ctrl+S) before closing the window.');
243
218
  }
244
- return result;
219
+ return saved;
245
220
  }
246
221
  //# sourceMappingURL=templates.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"templates.js","sourceRoot":"","sources":["../../src/windows/templates.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"templates.js","sourceRoot":"","sources":["../../src/windows/templates.ts"],"names":[],"mappings":"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","sourcesContent":["// Template emails: ordinary mail items kept in a mailbox folder and reused as\n// email bodies, plus the compose-window editor for one.\nimport { psBool, psInt, psString, readScriptOutput, runPowerShell, runPowerShellJson, scriptInput } from './run';\nimport { accountScript, DELIVERY_STORE_PS, FIND_FOLDER_PS, SESSION_PS } from './scripts';\nimport { bool, record, str, toArray } from '../shared/json';\nimport { findTemplateMarkers } from '../templateBody';\nimport { FolderId } from '../mail';\nimport { NotFoundError } from '../errors';\nimport { tempFile } from '../runtime';\nimport type { EditTemplateRequest, ReadTemplatesRequest, SaveTemplateRequest } from '../backend';\nimport type { SaveTemplateResult, TemplateFolderResult } from '../types';\n\n/** How deep under the mailbox root a template folder is searched for. */\nconst 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. */\nconst MAX_TEMPLATE_HTML = 100_000;\n\n/** Plain text standing in for a body that wasn't asked for. */\nconst PREVIEW_CHARS = 200;\n\n/**\n * The template emails in a mailbox folder, newest first. A missing folder is\n * reported as folderFound:false with the mailbox's top-level folder names\n * rather than thrown, so a caller can offer to create it.\n */\nexport async function readTemplateEmails(request: ReadTemplatesRequest): Promise<TemplateFolderResult> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n$root = $store.GetRootFolder()\n${FIND_FOLDER_PS}\n$folder = Find-FolderByName $root ${psString(request.folder)} ${psInt(TEMPLATE_SEARCH_DEPTH)}\nif ($folder -eq $null) {\n $names = @()\n foreach ($f in $root.Folders) { $names += [string]$f.Name }\n ConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{ folderFound = $false; folderPath = ''; templates = @(); availableFolders = @($names) })\n exit 0\n}\n$includeBody = ${psBool(request.includeBody)}\n$wanted = ${psString(request.subject ?? '')}\n$limit = ${psInt(request.limit)}\n$maxHtml = ${psInt(MAX_TEMPLATE_HTML)}\n$previewChars = ${psInt(PREVIEW_CHARS)}\n$items = $folder.Items\n$items.Sort('[LastModificationTime]', $true)\n$rows = @()\n$count = $items.Count\n# Without a subject the sort does the capping, newest first. With one, the whole\n# folder is scanned — the template wanted need not be among the newest.\nfor ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {\n $item = $null\n try { $item = $items.Item($i) } catch { continue }\n if ($item -eq $null) { continue }\n $class = 0\n try { $class = [int]$item.Class } catch {}\n if ($class -ne 43) { continue }\n $subject = ''\n try { $subject = ([string]$item.Subject).Trim() } catch {}\n if ($wanted -and $subject -ine $wanted) { continue }\n $html = ''\n $preview = ''\n $markers = ''\n if ($includeBody) {\n try { $html = [string]$item.HTMLBody } catch {}\n # Embedded (cid:) images live as attachments on the TEMPLATE item, so its\n # HTML reused elsewhere shows broken-image placeholders. Template emails\n # are text and markup only.\n $html = [regex]::Replace($html, '<img[^>]*src=\"cid:[^\"]*\"[^>]*>', '')\n if ($html.Length -gt $maxHtml) { $html = $html.Substring(0, $maxHtml) }\n } else {\n # No body asked for: a short preview, plus the section and placeholder\n # markers scanned here from the full plain text — so a caller can confirm\n # a template is intact without its body crossing stdout. Word can split a\n # marker across tags in the HTML but not in the plain text.\n $plain = ''\n try { $plain = [string]$item.Body } catch {}\n $found = @()\n foreach ($m in [regex]::Matches($plain, '\\\\[\\\\[\\\\s*/?\\\\s*[A-Za-z0-9_-]{1,40}\\\\s*\\\\]\\\\]|\\\\{\\\\{\\\\s*[A-Za-z0-9_-]{1,40}\\\\s*\\\\}\\\\}')) {\n $token = $m.Value -replace '\\\\s', ''\n if ($found -notcontains $token) { $found += $token }\n }\n $markers = $found -join ' '\n $preview = $plain.Trim()\n if ($preview.Length -gt $previewChars) { $preview = $preview.Substring(0, $previewChars) }\n }\n $modified = ''\n try { $modified = $item.LastModificationTime.ToString('yyyy-MM-dd HH:mm') } catch {}\n $rows += [PSCustomObject]@{\n entryId = [string]$item.EntryID\n subject = $subject\n htmlBody = $html\n bodyPreview = $preview\n markers = $markers\n lastModified = $modified\n }\n}\nConvertTo-Json -Depth 4 -InputObject ([PSCustomObject]@{\n folderFound = $true\n folderPath = [string]$folder.FolderPath\n templates = @($rows)\n availableFolders = @()\n})\n`, 'standard');\n const e = record(output);\n return {\n folderFound: bool(e.folderFound),\n folderPath: str(e.folderPath),\n templates: toArray(e.templates).map(row => {\n const t = record(row);\n const html = str(t.htmlBody);\n // With the HTML in hand it is authoritative (markers survive Word\n // splitting them across tags); without it, the markers the script\n // scanned out of the plain text stand in.\n const markers = findTemplateMarkers(request.includeBody ? html : str(t.markers));\n return {\n entryId: str(t.entryId),\n subject: str(t.subject),\n htmlBody: html,\n bodyPreview: str(t.bodyPreview),\n sections: markers.sections,\n placeholders: markers.placeholders,\n lastModified: str(t.lastModified),\n };\n }),\n availableFolders: toArray(e.availableFolders).map(str),\n };\n}\n\n/**\n * Save a new template email into a folder, creating the folder at the mailbox\n * root when absent. Always adds a new item — never overwrites one.\n */\nexport async function saveTemplateEmail(request: SaveTemplateRequest): Promise<SaveTemplateResult> {\n const body = scriptInput('template-body', request.htmlBody);\n try {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n$root = $store.GetRootFolder()\n${FIND_FOLDER_PS}\n$folderName = ${psString(request.folder)}\n$folder = Find-FolderByName $root $folderName ${psInt(TEMPLATE_SEARCH_DEPTH)}\n$folderCreated = $false\nif ($folder -eq $null) {\n $folder = $root.Folders.Add($folderName)\n $folderCreated = $true\n}\n$mail = $outlook.CreateItem(0)\n$mail.Subject = ${psString(request.subject)}\n$mail.HTMLBody = [IO.File]::ReadAllText(${psString(body.path)}, [Text.Encoding]::UTF8)\n$mail.Save()\n[void]$mail.Move($folder)\nConvertTo-Json -Compress -InputObject ([PSCustomObject]@{ folderPath = [string]$folder.FolderPath; folderCreated = $folderCreated })\n`, 'standard');\n const e = record(output);\n return {folderPath: str(e.folderPath), folderCreated: bool(e.folderCreated)};\n } finally {\n body.cleanup();\n }\n}\n\n/**\n * Open a compose window pre-filled with the HTML, wait for it to close, and\n * return what was saved (Ctrl+S) — or, failing a save, the body as it last\n * stood while the window was open. Leftover editor drafts are removed.\n */\nexport async function editEmailTemplate(request: EditTemplateRequest): Promise<string> {\n const input = scriptInput('template-input', request.html);\n const outputFile = tempFile('template-output', 'html');\n try {\n await runPowerShell(`${SESSION_PS}\n$subject = ${psString(`${request.label} - Save (Ctrl+S) and close when done`)}\n$outputPath = ${psString(outputFile)}\n$mail = $outlook.CreateItem(0)\n$mail.Subject = $subject\n$mail.HTMLBody = [IO.File]::ReadAllText(${psString(input.path)}, [Text.Encoding]::UTF8)\n$mail.Display()\nStart-Sleep -Seconds 2\n# Poll until the compose window closes, keeping the last live body as the\n# fallback for an edit that was never saved.\n$liveBody = ''\nwhile ($true) {\n Start-Sleep -Milliseconds 700\n $open = $false\n try {\n foreach ($inspector in $outlook.Inspectors) {\n $current = $null\n try { $current = $inspector.CurrentItem } catch {}\n if ($current -ne $null -and [string]$current.Subject -eq $subject) {\n $open = $true\n try { $liveBody = [string]$current.HTMLBody } catch {}\n break\n }\n }\n } catch {}\n if (-not $open) { break }\n}\n# Prefer the saved draft: it is what the person chose to keep, and it outlives\n# the window. Every editor draft is removed so none pile up in Drafts.\n$savedBody = ''\ntry {\n $draftItems = $ns.GetDefaultFolder(${psInt(FolderId.Drafts)}).Items\n for ($i = $draftItems.Count; $i -ge 1; $i--) {\n $draft = $draftItems.Item($i)\n if ([string]$draft.Subject -eq $subject) {\n if (-not $savedBody) { try { $savedBody = [string]$draft.HTMLBody } catch {} }\n try { $draft.Delete() } catch {}\n }\n }\n} catch {}\n$finalBody = if ($savedBody) { $savedBody } else { $liveBody }\nif ($finalBody) { [IO.File]::WriteAllText($outputPath, $finalBody, [Text.Encoding]::UTF8) }\n`, 'interactive');\n } finally {\n input.cleanup();\n }\n const saved = readScriptOutput(outputFile);\n if (!saved) {\n throw new NotFoundError('template', 'No template was saved. Save (Ctrl+S) before closing the window.');\n }\n return saved;\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dawnlit/outlook-bridge",
3
- "version": "1.1.0",
4
- "description": "Desktop Outlook automation for Node — read, send, reply, file, and template mail by driving the real Outlook client.",
3
+ "version": "2.0.0",
4
+ "description": "Desktop Outlook automation for Node — read, search, send, reply, file, and template mail by driving the real Outlook client on Windows and macOS.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dawnlit",
7
7
  "homepage": "https://github.com/dawnlit-ai/outlook-bridge#readme",
@@ -39,12 +39,13 @@
39
39
  "access": "public"
40
40
  },
41
41
  "scripts": {
42
- "build": "tsc -p .",
42
+ "clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
43
+ "build": "npm run clean && tsc -p .",
43
44
  "test": "npm run build && node --test \"test/*.test.js\"",
44
45
  "prepare": "npm run build"
45
46
  },
46
47
  "devDependencies": {
47
- "@types/node": "^24.0.0",
48
+ "@types/node": "^24.13.3",
48
49
  "typescript": "~6.0.3"
49
50
  }
50
51
  }
@@ -1,43 +0,0 @@
1
- import type { BridgeOptions, ResolvedConfig } from './runtime';
2
- import type { BridgeCapability, CapabilityMap, OutlookBridge } from './types';
3
- /** Which operations work on this machine. See `CapabilityMap`. */
4
- export declare function capabilities(): CapabilityMap;
5
- /** Whether one operation works here — `capabilities()` for a single name. */
6
- export declare function supports(operation: BridgeCapability): boolean;
7
- /**
8
- * An Outlook bridge carrying its own settings, plus capability discovery.
9
- *
10
- * The free functions exported below are the same automation against the
11
- * process-wide config; this is what you want when the process holds more than
12
- * one caller, or when a call needs its own timeout or AbortSignal.
13
- */
14
- export interface OutlookBridgeInstance extends OutlookBridge {
15
- /** The settings this instance runs with. */
16
- readonly options: Readonly<ResolvedConfig>;
17
- /** Which operations work on this machine. */
18
- capabilities(): CapabilityMap;
19
- /** Whether one operation works here. */
20
- supports(operation: BridgeCapability): boolean;
21
- /**
22
- * A bridge like this one with some settings changed — the per-call escape
23
- * hatch, and how a cancellable call is made:
24
- * `bridge.withOptions({ signal }).searchInboxByFilter(...)`.
25
- */
26
- withOptions(options: BridgeOptions): OutlookBridgeInstance;
27
- }
28
- /**
29
- * A bridge with its own settings, isolated from `configure()` and from every
30
- * other instance.
31
- *
32
- * ```ts
33
- * const bridge = createOutlookBridge({ timeoutMs: 30_000 });
34
- * if (bridge.supports('searchInboxByFilter')) {
35
- * await bridge.withOptions({ signal }).searchInboxByFilter(account, filter);
36
- * }
37
- * ```
38
- *
39
- * Options left out fall back to the process-wide values as they stand when this
40
- * is called.
41
- */
42
- export declare function createOutlookBridge(options?: BridgeOptions): OutlookBridgeInstance;
43
- export declare const getOutlookAccounts: () => Promise<string[]>, sendOutlookEmail: (params: import("./types").SendEmailParams) => Promise<void>, replyOutlookEmail: (params: import("./types").ReplyEmailParams) => Promise<import("./types").ReplyEmailResult>, sendAllDrafts: (emailAccount: string) => Promise<import("./types").SendAllDraftsResult>, readInboxEmails: (emailAccount: string, daysBack?: number, limit?: number, folder?: string) => Promise<import("./types").InboxEmail[]>, searchInboxByFilter: (emailAccount: string, filter?: import("./types").InboxSearchFilter, daysBack?: number) => Promise<import("./types").InboxSearchMatch[]>, readSelectedEmail: () => Promise<import("./types").SelectedEmail>, readEmailBody: (entryId: string, storeId?: string, maxChars?: number, includeQuoted?: boolean) => Promise<import("./types").EmailBodyResult>, openOutlookEmail: (entryId: string) => Promise<void>, listInboxFolders: (emailAccount: string, maxDepth?: number) => Promise<import("./types").InboxFolderInfo[]>, moveOutlookEmails: (emailAccount: string, entryIds: string[], folderName: string, createIfMissing?: boolean) => Promise<import("./types").MoveEmailsResult>, listOutlookDrafts: (emailAccount: string, limit?: number, previewChars?: number) => Promise<import("./types").ListDraftsResult>, deleteOutlookDrafts: (emailAccount: string, entryIds: string[]) => Promise<import("./types").DeleteDraftsResult>, sendDrafts: (emailAccount: string, entryIds: string[]) => Promise<import("./types").SendDraftsResult>, deleteOutlookEmails: (emailAccount: string, entryIds: string[], options?: import("./types").DeleteMailOptions) => Promise<import("./types").DeleteMailResult>, purgeDeletedItems: (emailAccount: string, olderThanDays?: number, dryRun?: boolean) => Promise<import("./types").PurgeDeletedItemsResult>, saveEmailAttachment: (entryId: string, fileName: string, storeId?: string, destDir?: string) => Promise<string>, saveEmailAttachmentDetailed: (entryId: string, fileName: string, storeId?: string, destDir?: string) => Promise<import("./types").SavedAttachment>, saveEmailAttachments: (entryId: string, fileNames: string[], storeId?: string, destDir?: string) => Promise<import("./types").SavedAttachment[]>, cleanUndeliverableEmails: (emailAccount: string, daysBack?: number, dryRun?: boolean) => Promise<import("./types").CleanUndeliverableResult>, collectBouncedRecipients: (emailAccount: string, daysBack?: number, scanDeleted?: boolean) => Promise<string[]>, readSentRecipientGroups: (emailAccount: string, daysBack?: number, limit?: number) => Promise<import("./types").SentRecipientGroup[]>, listOutlookSignatures: () => Promise<string[]>, readOutlookSignatureHtml: (name: string) => Promise<string>, readTemplateEmails: (emailAccount: string, folderName?: string, limit?: number, includeBody?: boolean, subject?: string) => Promise<import("./types").TemplateFolderResult>, saveTemplateEmail: (emailAccount: string, subject: string, htmlBody: string, folderName?: string) => Promise<import("./types").SaveTemplateResult>, editEmailTemplate: (label: string, currentHtml: string) => Promise<string>;
@@ -1,137 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.editEmailTemplate = exports.saveTemplateEmail = exports.readTemplateEmails = exports.readOutlookSignatureHtml = exports.listOutlookSignatures = exports.readSentRecipientGroups = exports.collectBouncedRecipients = exports.cleanUndeliverableEmails = exports.saveEmailAttachments = exports.saveEmailAttachmentDetailed = exports.saveEmailAttachment = exports.purgeDeletedItems = exports.deleteOutlookEmails = exports.sendDrafts = exports.deleteOutlookDrafts = exports.listOutlookDrafts = exports.moveOutlookEmails = exports.listInboxFolders = exports.openOutlookEmail = exports.readEmailBody = exports.readSelectedEmail = exports.searchInboxByFilter = exports.readInboxEmails = exports.sendAllDrafts = exports.replyOutlookEmail = exports.sendOutlookEmail = exports.getOutlookAccounts = void 0;
37
- exports.capabilities = capabilities;
38
- exports.supports = supports;
39
- exports.createOutlookBridge = createOutlookBridge;
40
- // Platform dispatcher for Outlook automation: Windows drives Outlook via
41
- // PowerShell + COM, macOS via AppleScript (see each implementation's index for
42
- // its platform notes — New Outlook for Mac supports composing but not mailbox
43
- // reading). Other platforms report no capabilities and every call rejects.
44
- //
45
- // Both implementations are pinned to `OutlookBridge` here rather than being
46
- // destructured straight off the module namespaces. That is the difference
47
- // between a consumer seeing `Promise<CleanUndeliverableResult>` and seeing
48
- // `Promise<CleanUndeliverableResult | { matched: unknown[] }>` — a union of
49
- // whatever the two platforms happened to declare, which is what this used to
50
- // publish.
51
- const windowsBridge = __importStar(require("./windows"));
52
- const macBridge = __importStar(require("./mac"));
53
- const runtime_1 = require("./runtime");
54
- const errors_1 = require("./errors");
55
- /** Every operation name, taken from the Windows bridge — the complete one. */
56
- const ALL_CAPABILITIES = Object.keys(windowsBridge.bridge);
57
- /**
58
- * The backend for an OS with no Outlook automation at all.
59
- *
60
- * A third implementation rather than a fallback to the PowerShell one: making
61
- * Windows the default for Linux and everything else meant the dispatcher held a
62
- * bridge whose capability map had to be second-guessed afterwards, and whose
63
- * functions each had to remember to refuse. Answering "not here" is itself an
64
- * implementation of the contract, so it is written as one, once.
65
- */
66
- const unsupportedBridge = {
67
- bridge: Object.fromEntries(ALL_CAPABILITIES.map(name => [
68
- name,
69
- () => Promise.reject(new errors_1.UnsupportedPlatformError(process.platform)),
70
- ])),
71
- capabilities: Object.freeze(Object.fromEntries(ALL_CAPABILITIES.map(name => [name, false]))),
72
- };
73
- // The platform decision, made once. Everything below reads `impl` and
74
- // `implCapabilities` without asking which OS this is.
75
- const { bridge: impl, capabilities: implCapabilities } = process.platform === 'win32' ? windowsBridge
76
- : process.platform === 'darwin' ? macBridge
77
- : unsupportedBridge;
78
- /** Which operations work on this machine. See `CapabilityMap`. */
79
- function capabilities() {
80
- return implCapabilities;
81
- }
82
- /** Whether one operation works here — `capabilities()` for a single name. */
83
- function supports(operation) {
84
- return implCapabilities[operation] === true;
85
- }
86
- /**
87
- * Build a bridge whose every call runs with `config` in force.
88
- *
89
- * The wrapper is what makes per-instance settings work at all: the platform
90
- * implementations read their config from a module-level accessor, so the config
91
- * has to be established around the call rather than passed into it. `withConfig`
92
- * puts it in an AsyncLocalStorage that the whole async subtree inherits, which is
93
- * why two bridges with different timeouts can run concurrently without seeing
94
- * each other's.
95
- */
96
- function build(config) {
97
- const instance = {
98
- capabilities,
99
- supports,
100
- withOptions: (options) => build((0, runtime_1.mergeOptions)(config, options)),
101
- options: Object.freeze({ ...config }),
102
- };
103
- const source = impl;
104
- for (const name of ALL_CAPABILITIES) {
105
- const fn = source[name];
106
- instance[name] = (...args) => {
107
- // Errors thrown synchronously (an argument check ahead of the first
108
- // await) become rejections here, so a caller has one failure channel.
109
- try {
110
- return (0, runtime_1.withConfig)(config, () => fn(...args));
111
- }
112
- catch (error) {
113
- return Promise.reject(error);
114
- }
115
- };
116
- }
117
- return instance;
118
- }
119
- /**
120
- * A bridge with its own settings, isolated from `configure()` and from every
121
- * other instance.
122
- *
123
- * ```ts
124
- * const bridge = createOutlookBridge({ timeoutMs: 30_000 });
125
- * if (bridge.supports('searchInboxByFilter')) {
126
- * await bridge.withOptions({ signal }).searchInboxByFilter(account, filter);
127
- * }
128
- * ```
129
- *
130
- * Options left out fall back to the process-wide values as they stand when this
131
- * is called.
132
- */
133
- function createOutlookBridge(options = {}) {
134
- return build((0, runtime_1.mergeOptions)((0, runtime_1.getGlobalConfig)(), options));
135
- }
136
- exports.getOutlookAccounts = impl.getOutlookAccounts, exports.sendOutlookEmail = impl.sendOutlookEmail, exports.replyOutlookEmail = impl.replyOutlookEmail, exports.sendAllDrafts = impl.sendAllDrafts, exports.readInboxEmails = impl.readInboxEmails, exports.searchInboxByFilter = impl.searchInboxByFilter, exports.readSelectedEmail = impl.readSelectedEmail, exports.readEmailBody = impl.readEmailBody, exports.openOutlookEmail = impl.openOutlookEmail, exports.listInboxFolders = impl.listInboxFolders, exports.moveOutlookEmails = impl.moveOutlookEmails, exports.listOutlookDrafts = impl.listOutlookDrafts, exports.deleteOutlookDrafts = impl.deleteOutlookDrafts, exports.sendDrafts = impl.sendDrafts, exports.deleteOutlookEmails = impl.deleteOutlookEmails, exports.purgeDeletedItems = impl.purgeDeletedItems, exports.saveEmailAttachment = impl.saveEmailAttachment, exports.saveEmailAttachmentDetailed = impl.saveEmailAttachmentDetailed, exports.saveEmailAttachments = impl.saveEmailAttachments, exports.cleanUndeliverableEmails = impl.cleanUndeliverableEmails, exports.collectBouncedRecipients = impl.collectBouncedRecipients, exports.readSentRecipientGroups = impl.readSentRecipientGroups, exports.listOutlookSignatures = impl.listOutlookSignatures, exports.readOutlookSignatureHtml = impl.readOutlookSignatureHtml, exports.readTemplateEmails = impl.readTemplateEmails, exports.saveTemplateEmail = impl.saveTemplateEmail, exports.editEmailTemplate = impl.editEmailTemplate;
137
- //# sourceMappingURL=OutlookService.js.map