@bobfrankston/rmfmail 1.0.664 → 1.0.666

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.
@@ -0,0 +1,2872 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // client/lib/api-client.js
12
+ var api_client_exports = {};
13
+ __export(api_client_exports, {
14
+ abortMessageListRequests: () => abortMessageListRequests,
15
+ addContact: () => addContact,
16
+ addPreferredContact: () => addPreferredContact,
17
+ addToDenylist: () => addToDenylist,
18
+ aiTransform: () => aiTransform,
19
+ allowRemoteContent: () => allowRemoteContent,
20
+ autocomplete: () => autocomplete,
21
+ cancelQueuedOutgoing: () => cancelQueuedOutgoing,
22
+ closeWordEdit: () => closeWordEdit,
23
+ connectEvents: () => connectEvents,
24
+ connectWebSocket: () => connectWebSocket,
25
+ consumePendingMailto: () => consumePendingMailto,
26
+ createCalendarEvent: () => createCalendarEvent,
27
+ createFolder: () => createFolder,
28
+ createTask: () => createTask,
29
+ deleteCalendarEvent: () => deleteCalendarEvent,
30
+ deleteContact: () => deleteContact,
31
+ deleteDraft: () => deleteDraft,
32
+ deleteFolder: () => deleteFolder,
33
+ deleteMessage: () => deleteMessage,
34
+ deleteMessages: () => deleteMessages,
35
+ deleteTask: () => deleteTask,
36
+ drainStoreSync: () => drainStoreSync,
37
+ emptyFolder: () => emptyFolder,
38
+ flagSenderOrDomain: () => flagSenderOrDomain,
39
+ formatJsonc: () => formatJsonc,
40
+ getAccounts: () => getAccounts,
41
+ getAttachment: () => getAttachment,
42
+ getAutocompleteSettings: () => getAutocompleteSettings,
43
+ getCalendarEvents: () => getCalendarEvents,
44
+ getDeviceAccounts: () => getDeviceAccounts,
45
+ getDiagnostics: () => getDiagnostics,
46
+ getFolders: () => getFolders,
47
+ getMessage: () => getMessage,
48
+ getMessages: () => getMessages,
49
+ getOutboxStatus: () => getOutboxStatus,
50
+ getPrimaryAccount: () => getPrimaryAccount,
51
+ getPriorityLists: () => getPriorityLists,
52
+ getSettings: () => getSettings,
53
+ getSyncPending: () => getSyncPending,
54
+ getTasks: () => getTasks,
55
+ getThreadMessages: () => getThreadMessages,
56
+ getUnifiedInbox: () => getUnifiedInbox,
57
+ getVersion: () => getVersion,
58
+ hasBccHistoryTo: () => hasBccHistoryTo,
59
+ hasCcHistoryTo: () => hasCcHistoryTo,
60
+ listContacts: () => listContacts,
61
+ listQueuedOutgoing: () => listQueuedOutgoing,
62
+ logClientEvent: () => logClientEvent,
63
+ markAsSpamMessages: () => markAsSpamMessages,
64
+ markFolderRead: () => markFolderRead,
65
+ moveMessage: () => moveMessage,
66
+ moveMessages: () => moveMessages,
67
+ onEvent: () => onEvent,
68
+ onWsEvent: () => onWsEvent,
69
+ openInWord: () => openInWord,
70
+ openLocalPath: () => openLocalPath,
71
+ readConfigHelp: () => readConfigHelp,
72
+ readJsoncFile: () => readJsoncFile,
73
+ reauthGoogleScopes: () => reauthGoogleScopes,
74
+ reauthenticate: () => reauthenticate,
75
+ recordSpamReport: () => recordSpamReport,
76
+ renameFolder: () => renameFolder,
77
+ repairAccounts: () => repairAccounts,
78
+ restartServer: () => restartServer,
79
+ saveAutocompleteSettings: () => saveAutocompleteSettings,
80
+ saveDraft: () => saveDraft,
81
+ saveSettings: () => saveSettings,
82
+ searchContacts: () => searchContacts,
83
+ searchMessages: () => searchMessages,
84
+ sendMessage: () => sendMessage,
85
+ setPriorityDomain: () => setPriorityDomain,
86
+ setPrioritySender: () => setPrioritySender,
87
+ setupAccount: () => setupAccount,
88
+ showReminderPopup: () => showReminderPopup,
89
+ syncAccount: () => syncAccount,
90
+ triggerSync: () => triggerSync,
91
+ undeleteMessage: () => undeleteMessage,
92
+ unsubscribeOneClick: () => unsubscribeOneClick,
93
+ updateCalendarEvent: () => updateCalendarEvent,
94
+ updateFlags: () => updateFlags,
95
+ updateTask: () => updateTask,
96
+ upsertContact: () => upsertContact,
97
+ writeJsoncFile: () => writeJsoncFile
98
+ });
99
+ function getIpc() {
100
+ if (typeof mailxapi !== "undefined" && mailxapi?.isApp)
101
+ return mailxapi;
102
+ if (window.opener?.mailxapi?.isApp)
103
+ return window.opener.mailxapi;
104
+ if (window.parent?.mailxapi?.isApp)
105
+ return window.parent.mailxapi;
106
+ return null;
107
+ }
108
+ function buildRelayBridge() {
109
+ const pending = /* @__PURE__ */ new Map();
110
+ window.addEventListener("message", (ev) => {
111
+ if (!ev.data || ev.data.type !== "mailx-ipc-result" || !ev.data.id)
112
+ return;
113
+ const entry = pending.get(ev.data.id);
114
+ if (!entry)
115
+ return;
116
+ pending.delete(ev.data.id);
117
+ clearTimeout(entry.timer);
118
+ if (ev.data.ok)
119
+ entry.resolve(ev.data.result);
120
+ else
121
+ entry.reject(new Error(ev.data.error || "parent-relay ipc error"));
122
+ });
123
+ const call = (method, args) => {
124
+ const id = `ipc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
125
+ return new Promise((resolve, reject) => {
126
+ const timer = setTimeout(() => {
127
+ pending.delete(id);
128
+ reject(new Error(`parent-relay timeout: ${method}`));
129
+ }, 12e4);
130
+ pending.set(id, { resolve, reject, timer });
131
+ try {
132
+ window.parent.postMessage({ type: "mailx-ipc", id, method, args }, "*");
133
+ } catch (e) {
134
+ clearTimeout(timer);
135
+ pending.delete(id);
136
+ reject(e);
137
+ }
138
+ });
139
+ };
140
+ return new Proxy({}, {
141
+ get(_t, prop) {
142
+ if (prop === "isApp")
143
+ return true;
144
+ if (prop === "platform")
145
+ return window.parent?.mailxapi?.platform || "webview2";
146
+ if (prop === "onEvent") {
147
+ return (handler) => window.parent?.mailxapi?.onEvent?.(handler);
148
+ }
149
+ return (...args) => call(prop, args);
150
+ }
151
+ });
152
+ }
153
+ function ipc() {
154
+ const inIframe = window.parent && window.parent !== window;
155
+ if (inIframe && window.parent?.mailxapi?.isApp) {
156
+ if (!cachedRelayBridge)
157
+ cachedRelayBridge = buildRelayBridge();
158
+ return cachedRelayBridge;
159
+ }
160
+ const bridge = getIpc();
161
+ if (!bridge)
162
+ throw new Error("IPC bridge not available");
163
+ return bridge;
164
+ }
165
+ function abortMessageListRequests() {
166
+ if (messageListAbort) {
167
+ messageListAbort.abort();
168
+ messageListAbort = null;
169
+ }
170
+ }
171
+ function getAccounts() {
172
+ return ipc().getAccounts();
173
+ }
174
+ function getFolders(accountId) {
175
+ return ipc().getFolders(accountId);
176
+ }
177
+ function getMessages(accountId, folderId, page = 1, pageSize = 50, flaggedOnly = false, sort, sortDir) {
178
+ abortMessageListRequests();
179
+ return ipc().getMessages(accountId, folderId, page, pageSize, sort, sortDir, void 0, flaggedOnly);
180
+ }
181
+ function getUnifiedInbox(page = 1, pageSize = 50) {
182
+ abortMessageListRequests();
183
+ return ipc().getUnifiedInbox(page, pageSize);
184
+ }
185
+ function searchMessages(query, page = 1, pageSize = 50, scope = "all", accountId = "", folderId = 0, includeTrashSpam = false) {
186
+ return ipc().searchMessages(query, page, pageSize, scope, accountId, folderId, includeTrashSpam);
187
+ }
188
+ function getMessage(accountId, uid, allowRemote = false, folderId) {
189
+ return ipc().getMessage(accountId, uid, allowRemote, folderId);
190
+ }
191
+ function updateFlags(accountId, uid, flags) {
192
+ return ipc().updateFlags(accountId, uid, flags);
193
+ }
194
+ function triggerSync() {
195
+ return ipc().syncAll();
196
+ }
197
+ function syncAccount(accountId) {
198
+ return ipc().syncAccount(accountId);
199
+ }
200
+ function reauthenticate(accountId) {
201
+ return ipc().reauthenticate(accountId);
202
+ }
203
+ function reauthGoogleScopes() {
204
+ return ipc().reauthGoogleScopes();
205
+ }
206
+ function getSyncPending() {
207
+ return ipc().getSyncPending();
208
+ }
209
+ function getDiagnostics() {
210
+ return ipc().getDiagnostics?.() ?? Promise.resolve([]);
211
+ }
212
+ function getPrimaryAccount(feature) {
213
+ return ipc().getPrimaryAccount?.(feature) ?? Promise.resolve(null);
214
+ }
215
+ function getCalendarEvents(fromMs, toMs) {
216
+ return ipc().getCalendarEvents?.(fromMs, toMs) ?? Promise.resolve([]);
217
+ }
218
+ function createCalendarEvent(ev) {
219
+ return ipc().createCalendarEvent?.(ev);
220
+ }
221
+ function updateCalendarEvent(uuid, patch) {
222
+ return ipc().updateCalendarEvent?.(uuid, patch);
223
+ }
224
+ function deleteCalendarEvent(uuid) {
225
+ return ipc().deleteCalendarEvent?.(uuid);
226
+ }
227
+ function getTasks(includeCompleted = false) {
228
+ return ipc().getTasks?.(includeCompleted) ?? Promise.resolve([]);
229
+ }
230
+ function createTask(t) {
231
+ return ipc().createTask?.(t);
232
+ }
233
+ function updateTask(uuid, patch) {
234
+ return ipc().updateTask?.(uuid, patch);
235
+ }
236
+ function deleteTask(uuid) {
237
+ return ipc().deleteTask?.(uuid);
238
+ }
239
+ function drainStoreSync() {
240
+ return ipc().drainStoreSync?.();
241
+ }
242
+ function recordSpamReport(accountId, uid, folderId) {
243
+ return ipc().recordSpamReport?.(accountId, uid, folderId);
244
+ }
245
+ function getOutboxStatus() {
246
+ return ipc().getOutboxStatus();
247
+ }
248
+ function listQueuedOutgoing() {
249
+ return ipc().listQueuedOutgoing();
250
+ }
251
+ function cancelQueuedOutgoing(p) {
252
+ return ipc().cancelQueuedOutgoing(p);
253
+ }
254
+ function searchContacts(query) {
255
+ return ipc().searchContacts(query);
256
+ }
257
+ function hasCcHistoryTo(email) {
258
+ return ipc().hasCcHistoryTo(email);
259
+ }
260
+ function hasBccHistoryTo(email) {
261
+ return ipc().hasBccHistoryTo(email);
262
+ }
263
+ function listContacts(query, page = 1, pageSize = 100) {
264
+ return ipc().listContacts(query, page, pageSize);
265
+ }
266
+ function upsertContact(name, email) {
267
+ return ipc().upsertContact(name, email);
268
+ }
269
+ function deleteContact(email) {
270
+ return ipc().deleteContact(email);
271
+ }
272
+ function addPreferredContact(entry) {
273
+ return ipc().addPreferredContact(entry.name, entry.email, entry.source, entry.organization);
274
+ }
275
+ function getPriorityLists() {
276
+ return ipc().getPriorityLists();
277
+ }
278
+ function setPrioritySender(email, value, name) {
279
+ return ipc().setPrioritySender(email, value, name);
280
+ }
281
+ function setPriorityDomain(domain, value) {
282
+ return ipc().setPriorityDomain(domain, value);
283
+ }
284
+ function addToDenylist(email) {
285
+ return ipc().addToDenylist(email);
286
+ }
287
+ function openLocalPath(which) {
288
+ return ipc().openLocalPath(which);
289
+ }
290
+ function allowRemoteContent(type, value) {
291
+ return ipc().allowRemoteContent(type, value);
292
+ }
293
+ function flagSenderOrDomain(type, value) {
294
+ return ipc().flagSenderOrDomain?.(type, value) ?? Promise.resolve({ flagged: false });
295
+ }
296
+ function deleteMessage(accountId, uid) {
297
+ return ipc().deleteMessage?.(accountId, uid);
298
+ }
299
+ function deleteMessages(accountId, uids) {
300
+ if (uids.length === 1)
301
+ return deleteMessage(accountId, uids[0]);
302
+ return ipc().deleteMessages?.(accountId, uids);
303
+ }
304
+ function moveMessages(accountId, uids, targetFolderId, targetAccountId) {
305
+ if (uids.length === 1)
306
+ return moveMessage(accountId, uids[0], targetFolderId, targetAccountId);
307
+ return ipc().moveMessages?.(accountId, uids, targetFolderId, targetAccountId);
308
+ }
309
+ function markAsSpamMessages(accountId, uids) {
310
+ return ipc().markAsSpamMessages?.(accountId, uids);
311
+ }
312
+ function undeleteMessage(accountId, uid, folderId) {
313
+ return ipc().undeleteMessage?.(accountId, uid, folderId);
314
+ }
315
+ function moveMessage(accountId, uid, targetFolderId, targetAccountId) {
316
+ return ipc().moveMessage?.(accountId, uid, targetFolderId, targetAccountId);
317
+ }
318
+ function restartServer() {
319
+ return ipc().restart?.();
320
+ }
321
+ function markFolderRead(accountId, folderId) {
322
+ return ipc().markFolderRead?.(accountId, folderId);
323
+ }
324
+ function createFolder(accountId, parentPath, name) {
325
+ return ipc().createFolder?.(accountId, parentPath, name);
326
+ }
327
+ function renameFolder(accountId, folderId, newName) {
328
+ return ipc().renameFolder?.(accountId, folderId, newName);
329
+ }
330
+ function deleteFolder(accountId, folderId) {
331
+ return ipc().deleteFolder?.(accountId, folderId);
332
+ }
333
+ function emptyFolder(accountId, folderId) {
334
+ return ipc().emptyFolder?.(accountId, folderId);
335
+ }
336
+ function logClientEvent(tag, data) {
337
+ let delivered = false;
338
+ try {
339
+ const bridge = typeof globalThis.mailxapi !== "undefined" && globalThis.mailxapi?.isApp ? globalThis.mailxapi : window.opener?.mailxapi?.isApp ? window.opener.mailxapi : window.parent?.mailxapi?.isApp ? window.parent.mailxapi : null;
340
+ if (bridge?.logClientEvent) {
341
+ bridge.logClientEvent(tag, data);
342
+ delivered = true;
343
+ }
344
+ } catch {
345
+ }
346
+ try {
347
+ if (window.parent && window.parent !== window) {
348
+ window.parent.postMessage({ type: "mailx-trace", tag, data, bridged: delivered }, "*");
349
+ }
350
+ } catch {
351
+ }
352
+ }
353
+ function sendMessage(body) {
354
+ return ipc().sendMessage?.(body);
355
+ }
356
+ function saveDraft(body) {
357
+ return ipc().saveDraft?.(body);
358
+ }
359
+ function onEvent(handler) {
360
+ eventHandlers.push(handler);
361
+ return () => {
362
+ const i = eventHandlers.indexOf(handler);
363
+ if (i >= 0)
364
+ eventHandlers.splice(i, 1);
365
+ };
366
+ }
367
+ function connectEvents() {
368
+ ipc().onEvent((event) => {
369
+ for (const h of eventHandlers)
370
+ h(event);
371
+ });
372
+ }
373
+ function autocomplete(body, signal) {
374
+ return ipc().autocomplete?.(body);
375
+ }
376
+ function getAutocompleteSettings() {
377
+ return ipc().getAutocompleteSettings?.();
378
+ }
379
+ function saveAutocompleteSettings(settings) {
380
+ return ipc().saveAutocompleteSettings?.(settings);
381
+ }
382
+ function getVersion() {
383
+ return ipc().getVersion();
384
+ }
385
+ function getSettings() {
386
+ return ipc().getSettings();
387
+ }
388
+ function saveSettings(settings) {
389
+ return ipc().saveSettingsData?.(settings);
390
+ }
391
+ function repairAccounts() {
392
+ return ipc().repairAccounts?.();
393
+ }
394
+ function deleteDraft(accountId, draftUid2, draftId2) {
395
+ return ipc().deleteDraft?.(accountId, draftUid2, draftId2);
396
+ }
397
+ function addContact(name, email) {
398
+ return ipc().addContact?.(name, email);
399
+ }
400
+ function getThreadMessages(accountId, threadId) {
401
+ return ipc().getThreadMessages?.(accountId, threadId);
402
+ }
403
+ function readJsoncFile(name) {
404
+ return ipc().readJsoncFile?.(name);
405
+ }
406
+ function writeJsoncFile(name, content) {
407
+ return ipc().writeJsoncFile?.(name, content);
408
+ }
409
+ function formatJsonc(content) {
410
+ return ipc().formatJsonc?.(content);
411
+ }
412
+ function readConfigHelp(name) {
413
+ return ipc().readConfigHelp?.(name) ?? Promise.resolve({ content: "" });
414
+ }
415
+ function unsubscribeOneClick(url) {
416
+ return ipc().unsubscribeOneClick?.(url);
417
+ }
418
+ function openInWord(editId, html) {
419
+ return ipc().openInWord?.(editId, html) ?? Promise.resolve({ ok: false, path: "", opener: "none" });
420
+ }
421
+ function closeWordEdit(editId) {
422
+ return ipc().closeWordEdit?.(editId) ?? Promise.resolve();
423
+ }
424
+ function showReminderPopup(opts) {
425
+ return ipc().showReminderPopup?.(opts) ?? Promise.resolve({ button: "", reason: "no host" });
426
+ }
427
+ function consumePendingMailto() {
428
+ return ipc().consumePendingMailto?.() ?? Promise.resolve(null);
429
+ }
430
+ function aiTransform(req) {
431
+ return ipc().aiTransform?.(req) ?? Promise.resolve({ text: "", reason: "AI not available in this host" });
432
+ }
433
+ function setupAccount(name, email, password) {
434
+ return ipc().setupAccount?.(name, email, password);
435
+ }
436
+ async function getAttachment(accountId, uid, attachmentId, folderId) {
437
+ return ipc().getAttachment(accountId, uid, attachmentId, folderId);
438
+ }
439
+ async function getDeviceAccounts() {
440
+ return ipc().getDeviceAccounts?.() ?? [];
441
+ }
442
+ var cachedRelayBridge, messageListAbort, eventHandlers, connectWebSocket, onWsEvent;
443
+ var init_api_client = __esm({
444
+ "client/lib/api-client.js"() {
445
+ "use strict";
446
+ cachedRelayBridge = null;
447
+ messageListAbort = null;
448
+ eventHandlers = [];
449
+ connectWebSocket = connectEvents;
450
+ onWsEvent = onEvent;
451
+ }
452
+ });
453
+
454
+ // client/lib/rmf-tiny.js
455
+ var rmf_tiny_exports = {};
456
+ __export(rmf_tiny_exports, {
457
+ createTinyMceEditor: () => createTinyMceEditor
458
+ });
459
+ async function loadTinymce(opts) {
460
+ try {
461
+ const mod = await import(
462
+ /* @vite-ignore */
463
+ "tinymce"
464
+ );
465
+ const tinymce = mod.default || mod;
466
+ await Promise.all([
467
+ import("tinymce/themes/silver").catch(() => {
468
+ }),
469
+ import("tinymce/icons/default").catch(() => {
470
+ }),
471
+ import("tinymce/models/dom").catch(() => {
472
+ }),
473
+ import("tinymce/plugins/paste").catch(() => {
474
+ }),
475
+ import("tinymce/plugins/lists").catch(() => {
476
+ }),
477
+ import("tinymce/plugins/link").catch(() => {
478
+ }),
479
+ import("tinymce/plugins/table").catch(() => {
480
+ }),
481
+ import("tinymce/plugins/code").catch(() => {
482
+ }),
483
+ import("tinymce/plugins/image").catch(() => {
484
+ })
485
+ ]);
486
+ return tinymce;
487
+ } catch {
488
+ }
489
+ const w = window;
490
+ if (w.tinymce)
491
+ return w.tinymce;
492
+ if (!opts.cdnUrl) {
493
+ throw new Error("rmf-tiny: tinymce not installed (npm install tinymce) and no cdnUrl supplied. See README for Android setup.");
494
+ }
495
+ const url = opts.apiKey && !opts.cdnUrl.includes("api-key") ? `${opts.cdnUrl}${opts.cdnUrl.includes("?") ? "&" : "?"}apiKey=${encodeURIComponent(opts.apiKey)}` : opts.cdnUrl;
496
+ await new Promise((resolve, reject) => {
497
+ const s = document.createElement("script");
498
+ s.src = url;
499
+ s.referrerPolicy = "origin";
500
+ s.onload = () => resolve();
501
+ s.onerror = () => reject(new Error(`rmf-tiny: failed to load TinyMCE from ${url}`));
502
+ document.head.appendChild(s);
503
+ });
504
+ if (!w.tinymce)
505
+ throw new Error("rmf-tiny: TinyMCE script loaded but window.tinymce is missing");
506
+ return w.tinymce;
507
+ }
508
+ async function createTinyMceEditor(container2, opts = {}) {
509
+ const tinymce = await loadTinymce(opts);
510
+ const target = document.createElement("div");
511
+ target.id = `rmf-tiny-${Math.random().toString(36).slice(2, 10)}`;
512
+ target.style.cssText = "width:100%;height:100%;";
513
+ container2.appendChild(target);
514
+ const editor2 = await new Promise((resolve) => {
515
+ tinymce.init({
516
+ target,
517
+ // Word-paste fidelity is the entire point of using TinyMCE here.
518
+ // `paste_as_text: false` keeps formatting; powerpaste isn't in
519
+ // OSS but the standard paste plugin handles Word reasonably well.
520
+ // All free / OSS plugins bundled in the jsDelivr tinymce@6 script.
521
+ // No premium plugins listed — listing one without an API key
522
+ // triggers TinyMCE's upsell dialog on every load.
523
+ plugins: "paste lists link table code image searchreplace autolink wordcount emoticons charmap insertdatetime quickbars nonbreaking directionality",
524
+ toolbar: [
525
+ "undo redo | bold italic underline strikethrough | forecolor backcolor",
526
+ "bullist numlist outdent indent | link table image code | emoticons charmap"
527
+ ].join(" | "),
528
+ // Include "tools" so wordcount and searchreplace are reachable.
529
+ menubar: "file edit view insert format tools",
530
+ // View menu — append zoom commands. fullscreen omitted: this editor
531
+ // lives inside a compose iframe overlay that already fills its host
532
+ // window; TinyMCE's fullscreen plugin re-positions the container in
533
+ // ways that produce a blank body, so it's not in the plugin list.
534
+ menu: {
535
+ view: { title: "View", items: "code | visualaid visualchars visualblocks | preview | zoomIn zoomOut zoomReset" }
536
+ },
537
+ // Disable the "Get all features" upsell badge that TinyMCE 6+
538
+ // injects automatically when no premium plugins are listed.
539
+ promotion: false,
540
+ // Floating toolbar that appears on text selection — keeps the
541
+ // main toolbar uncluttered while exposing common formatters
542
+ // where the cursor already is. quickbars_insert_toolbar:false
543
+ // suppresses the second (insert-on-blank-line) popup which
544
+ // clutters more than it helps in an email composer.
545
+ quickbars_selection_toolbar: "bold italic underline | forecolor | quicklink blockquote",
546
+ quickbars_insert_toolbar: false,
547
+ quickbars_image_toolbar: "alignleft aligncenter alignright",
548
+ // WebView's native spell-check (red underlines, right-click
549
+ // "Add to dictionary"). Free; same UX as Quill's spellcheck=true.
550
+ // Premium tinymcespellchecker plugin would replace this with a
551
+ // custom backend via spellchecker_rpc_url, but requires a
552
+ // Tiny Cloud subscription.
553
+ browser_spellcheck: true,
554
+ // Right-click context menu DISABLED so WebView2's native menu
555
+ // fires instead. We need the native menu because that's where
556
+ // the browser shows spelling suggestions (TinyMCE's contextmenu
557
+ // intercepts the right-click and replaces the menu — red
558
+ // underlines appear but suggestions are unreachable). Trade-off:
559
+ // lose TinyMCE's link / image / table quick actions. Standard
560
+ // text formatting is still on the toolbar.
561
+ contextmenu: false,
562
+ statusbar: false,
563
+ branding: false,
564
+ license_key: "gpl",
565
+ paste_data_images: true,
566
+ // Permissive valid_elements — preserve as much of the source
567
+ // formatting as possible. The default is more aggressive about
568
+ // stripping. Empty string for `valid_elements` means accept
569
+ // everything that the schema allows.
570
+ paste_word_valid_elements: "@[style|class],-strong/b,-em/i,-u,-s,-sub,-sup,-strike,-p,-ol,-ul,-li,-h1,-h2,-h3,-h4,-h5,-h6,-blockquote,-table[border|cellpadding|cellspacing|width|height|class|style],-tr,-td[colspan|rowspan|width|height|class|style|valign|align|background|bgcolor],-th,-thead,-tbody,-tfoot,-pre,-br,-a[href|target|title],-img[src|alt|width|height|style|class]",
571
+ paste_retain_style_properties: "color background background-color font-family font-size font-weight font-style text-decoration text-align padding padding-top padding-bottom padding-left padding-right margin margin-top margin-bottom margin-left margin-right border border-top border-bottom border-left border-right",
572
+ content_style: "body { font-family: system-ui, sans-serif; font-size: 14px; }",
573
+ init_instance_callback: (ed) => resolve(ed),
574
+ setup: (ed) => {
575
+ if (opts.initialHtml)
576
+ ed.on("init", () => ed.setContent(opts.initialHtml));
577
+ const ZOOM_DEFAULT = 14;
578
+ const ZOOM_STEP = 2;
579
+ const ZOOM_MIN = 8;
580
+ const ZOOM_MAX = 32;
581
+ let zoomPx = ZOOM_DEFAULT;
582
+ const applyZoom = () => {
583
+ try {
584
+ const body = ed.getBody();
585
+ if (body)
586
+ body.style.fontSize = `${zoomPx}px`;
587
+ } catch {
588
+ }
589
+ };
590
+ const bumpZoom = (delta) => {
591
+ zoomPx = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, zoomPx + delta));
592
+ applyZoom();
593
+ };
594
+ ed.ui.registry.addMenuItem("zoomIn", {
595
+ text: "Zoom in",
596
+ shortcut: "Ctrl+=",
597
+ onAction: () => bumpZoom(ZOOM_STEP)
598
+ });
599
+ ed.ui.registry.addMenuItem("zoomOut", {
600
+ text: "Zoom out",
601
+ shortcut: "Ctrl+-",
602
+ onAction: () => bumpZoom(-ZOOM_STEP)
603
+ });
604
+ ed.ui.registry.addMenuItem("zoomReset", {
605
+ text: "Reset zoom",
606
+ shortcut: "Ctrl+0",
607
+ onAction: () => {
608
+ zoomPx = ZOOM_DEFAULT;
609
+ applyZoom();
610
+ }
611
+ });
612
+ ed.on("init", () => {
613
+ try {
614
+ const body = ed.getBody();
615
+ if (body) {
616
+ body.setAttribute("spellcheck", "true");
617
+ body.setAttribute("lang", "en");
618
+ }
619
+ const doc = ed.getDoc();
620
+ if (doc?.documentElement)
621
+ doc.documentElement.setAttribute("lang", "en");
622
+ } catch {
623
+ }
624
+ try {
625
+ const doc = ed.getDoc();
626
+ doc.addEventListener("wheel", (e) => {
627
+ if (!e.ctrlKey)
628
+ return;
629
+ e.preventDefault();
630
+ bumpZoom(e.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP);
631
+ }, { passive: false });
632
+ doc.addEventListener("keydown", (e) => {
633
+ if (!(e.ctrlKey || e.metaKey))
634
+ return;
635
+ if (e.key === "=" || e.key === "+") {
636
+ e.preventDefault();
637
+ e.stopPropagation();
638
+ bumpZoom(ZOOM_STEP);
639
+ } else if (e.key === "-") {
640
+ e.preventDefault();
641
+ e.stopPropagation();
642
+ bumpZoom(-ZOOM_STEP);
643
+ } else if (e.key === "0") {
644
+ e.preventDefault();
645
+ e.stopPropagation();
646
+ zoomPx = ZOOM_DEFAULT;
647
+ applyZoom();
648
+ }
649
+ }, true);
650
+ } catch {
651
+ }
652
+ });
653
+ }
654
+ });
655
+ });
656
+ return {
657
+ setHtml(html) {
658
+ editor2.setContent(html);
659
+ },
660
+ getHtml() {
661
+ return editor2.getContent();
662
+ },
663
+ getText() {
664
+ return editor2.getContent({ format: "text" });
665
+ },
666
+ focus() {
667
+ editor2.focus();
668
+ },
669
+ setCursor(_pos) {
670
+ try {
671
+ editor2.selection.select(editor2.getBody(), true);
672
+ editor2.selection.collapse(false);
673
+ } catch {
674
+ }
675
+ },
676
+ get root() {
677
+ return editor2.getContainer();
678
+ },
679
+ on(event, handler) {
680
+ editor2.on(event, handler);
681
+ },
682
+ off(event, handler) {
683
+ editor2.off(event, handler);
684
+ }
685
+ };
686
+ }
687
+ var init_rmf_tiny = __esm({
688
+ "client/lib/rmf-tiny.js"() {
689
+ "use strict";
690
+ }
691
+ });
692
+
693
+ // client/compose/ghost-text.js
694
+ var ghost_text_exports = {};
695
+ __export(ghost_text_exports, {
696
+ destroyGhostText: () => destroyGhostText,
697
+ initGhostText: () => initGhostText
698
+ });
699
+ function dismiss() {
700
+ currentSuggestion = null;
701
+ if (ghostEl) {
702
+ ghostEl.remove();
703
+ ghostEl = null;
704
+ }
705
+ }
706
+ function positionGhost(editor2, text) {
707
+ dismiss();
708
+ const sel = window.getSelection();
709
+ if (!sel || sel.rangeCount === 0 || !sel.isCollapsed)
710
+ return;
711
+ const range = sel.getRangeAt(0);
712
+ let rect = range.getBoundingClientRect();
713
+ if (rect.width === 0 && rect.height === 0) {
714
+ const span = document.createElement("span");
715
+ span.textContent = "\u200B";
716
+ range.insertNode(span);
717
+ rect = span.getBoundingClientRect();
718
+ span.remove();
719
+ sel.removeAllRanges();
720
+ sel.addRange(range);
721
+ }
722
+ const container2 = editor2.getScrollContainer();
723
+ const containerRect = container2.getBoundingClientRect();
724
+ ghostEl = document.createElement("span");
725
+ ghostEl.className = "ghost-text";
726
+ ghostEl.textContent = text;
727
+ ghostEl.style.top = `${rect.top - containerRect.top + container2.scrollTop}px`;
728
+ ghostEl.style.left = `${rect.left - containerRect.left + container2.scrollLeft}px`;
729
+ container2.appendChild(ghostEl);
730
+ currentSuggestion = text;
731
+ }
732
+ function requestSuggestion(editor2, context) {
733
+ if (abortController) {
734
+ abortController.abort();
735
+ abortController = null;
736
+ }
737
+ const bodyText = editor2.getText();
738
+ if (!bodyText || bodyText.trim().length < 3)
739
+ return;
740
+ abortController = new AbortController();
741
+ const signal = abortController.signal;
742
+ autocomplete({
743
+ subject: context.getSubject(),
744
+ to: context.getTo(),
745
+ bodyText,
746
+ cursorOffset: bodyText.length
747
+ }, signal).then((result) => {
748
+ if (signal.aborted)
749
+ return;
750
+ if (result.suggestion) {
751
+ positionGhost(editor2, result.suggestion);
752
+ }
753
+ }).catch((e) => {
754
+ if (e.name === "AbortError")
755
+ return;
756
+ });
757
+ }
758
+ function initGhostText(editor2, context, options) {
759
+ activeEditor = editor2;
760
+ if (options?.debounceMs)
761
+ debounceMs = options.debounceMs;
762
+ editor2.onContentChange(() => {
763
+ dismiss();
764
+ if (debounceTimer)
765
+ clearTimeout(debounceTimer);
766
+ debounceTimer = setTimeout(() => {
767
+ requestSuggestion(editor2, context);
768
+ }, debounceMs);
769
+ });
770
+ editor2.onKeyDown((e) => {
771
+ if (!currentSuggestion)
772
+ return;
773
+ if (e.key === "Tab") {
774
+ e.preventDefault();
775
+ e.stopPropagation();
776
+ const text = currentSuggestion;
777
+ dismiss();
778
+ editor2.insertTextAtCursor(text);
779
+ return;
780
+ }
781
+ if (e.key === "Escape") {
782
+ e.preventDefault();
783
+ e.stopPropagation();
784
+ dismiss();
785
+ return;
786
+ }
787
+ dismiss();
788
+ });
789
+ editor2.root.addEventListener("blur", dismiss);
790
+ editor2.getScrollContainer().addEventListener("scroll", dismiss);
791
+ }
792
+ function destroyGhostText() {
793
+ dismiss();
794
+ if (debounceTimer)
795
+ clearTimeout(debounceTimer);
796
+ if (abortController)
797
+ abortController.abort();
798
+ activeEditor = null;
799
+ }
800
+ var ghostEl, currentSuggestion, debounceTimer, abortController, activeEditor, debounceMs;
801
+ var init_ghost_text = __esm({
802
+ "client/compose/ghost-text.js"() {
803
+ "use strict";
804
+ init_api_client();
805
+ ghostEl = null;
806
+ currentSuggestion = null;
807
+ debounceTimer = null;
808
+ abortController = null;
809
+ activeEditor = null;
810
+ debounceMs = 600;
811
+ }
812
+ });
813
+
814
+ // client/compose/editor-help.js
815
+ var editor_help_exports = {};
816
+ __export(editor_help_exports, {
817
+ EDITOR_HELP_MD: () => EDITOR_HELP_MD
818
+ });
819
+ var EDITOR_HELP_MD;
820
+ var init_editor_help = __esm({
821
+ "client/compose/editor-help.js"() {
822
+ "use strict";
823
+ EDITOR_HELP_MD = `# Compose editor \u2014 formatting and shortcuts
824
+
825
+ mailx ships with two rich-text editors: **Quill** (default) and **tiptap**.
826
+ Most things work the same in both. Differences are called out below.
827
+
828
+ Switch editors via **Settings \u2192 Editor \u2192 Quill | tiptap**.
829
+
830
+ ## Universal \u2014 works in both editors
831
+
832
+ | Action | Shortcut | Where |
833
+ |---|---|---|
834
+ | **Send** | Ctrl+Enter | toolbar Send button |
835
+ | Bold / Italic / Underline | Ctrl+B / Ctrl+I / Ctrl+U | toolbar B / I / U |
836
+ | Strikethrough | Ctrl+Shift+X | toolbar S |
837
+ | Bulleted list | Ctrl+Shift+8 | toolbar \u2022 |
838
+ | Ordered list | Ctrl+Shift+7 | toolbar 1. |
839
+ | Insert / edit link | Ctrl+K | toolbar \u{1F517} |
840
+ | Remove link | Ctrl+Shift+K | (no toolbar button \u2014 use Ctrl+Shift+K) |
841
+ | Blockquote | (Quill: Ctrl+Shift+Q; tiptap: toolbar) | toolbar " |
842
+ | Clear formatting | Ctrl+\\\\ | toolbar \u2716 |
843
+ | Heading (H1 / H2 / H3) | (Quill: format dropdown; tiptap: select dropdown) | "Normal / Heading 1 / 2 / 3" |
844
+ | Undo / Redo | Ctrl+Z / Ctrl+Y | (no toolbar button) |
845
+ | Spell-check | (browser native \u2014 red underlines) | right-click word |
846
+ | Paste plain text | Ctrl+Shift+V | (browser native) |
847
+
848
+ ## Quill-only
849
+
850
+ | Action | Shortcut |
851
+ |---|---|
852
+ | Indent / outdent | Ctrl+] / Ctrl+[ |
853
+ | Color text | Ctrl+Shift+C |
854
+ | Format dropdown | toolbar (left side) |
855
+ | Inline code | toolbar \`<>\` |
856
+ | Code block | toolbar |
857
+ | Image inserter | (no built-in; use drag-and-drop or paste) |
858
+
859
+ Quill has a more elaborate toolbar with format-specific dropdowns (font, size,
860
+ color). Internally Quill uses its own *Delta* document model \u2014 copy/paste
861
+ from Word/Outlook sometimes leaves extra empty paragraphs that you'll see
862
+ in the sent message body.
863
+
864
+ ## tiptap-only
865
+
866
+ | Action | Where |
867
+ |---|---|
868
+ | Heading select | left of toolbar |
869
+ | Toggle bold / italic / underline / strike | toolbar B / I / U / S |
870
+ | Blockquote | toolbar " |
871
+ | Image (drag-and-drop) | drop a file into the body |
872
+
873
+ tiptap is built on ProseMirror. Output HTML is cleaner than Quill on
874
+ Word/Outlook paste roundtrips. Bundle is smaller. Some Quill toolbar
875
+ features (inline code, indent shortcuts, color picker) aren't wired \u2014
876
+ use the heading select / format menu instead.
877
+
878
+ ## Common features (across both)
879
+
880
+ - **Drag-and-drop attachments** \u2014 drop files anywhere on the compose
881
+ window to attach. Overlay highlights the drop target while dragging.
882
+ - **Edit in Word / LibreOffice** \u2014 toolbar **Edit in Word** button opens
883
+ the body in your default external editor. Save in the external editor
884
+ and the body reloads here. mailx writes a temporary \`.docx\` file (see
885
+ \`~/.rmfmail/external-edit/\`) and watches it for changes.
886
+ - **Auto-save drafts** \u2014 every 5 seconds (and on input debounce / on
887
+ blur). Drafts land in the Drafts folder via IMAP append.
888
+ - **Address auto-completion** \u2014 type a partial name in To/Cc/Bcc; matches
889
+ rank by recency \xD7 use-count. Group names from \`contacts.jsonc \u2192 groups\`
890
+ also surface here.
891
+ - **Address-field expansion** \u2014 recipient fields are auto-growing
892
+ textareas; long lists wrap to multiple lines.
893
+ - **Group expansion on send** \u2014 type a group name (e.g. \`family\`) in
894
+ To/Cc/Bcc and it expands to the address list at send time.
895
+ - **Ghost-text autocomplete** (off by default) \u2014 Settings \u2192
896
+ AI autocomplete \u2192 on. Predicts the next words while you type.
897
+
898
+ ## When the toolbar doesn't appear
899
+
900
+ The editor loads from a CDN (jsdelivr). If your network can't reach it, the
901
+ toolbar disappears and a plain \`contenteditable\` fallback takes over. Status
902
+ bar shows the failure. mailx tries the *other* editor before falling all the
903
+ way back; if both fail you get a plain textarea with no shortcuts and no
904
+ toolbar \u2014 sending still works.
905
+
906
+ ## See also
907
+
908
+ - \`accounts.md\`, \`contacts.md\`, \`allowlist.md\`, \`clients.md\`, \`config.md\`,
909
+ \`preferences.md\` \u2014 config-file references (these live in your GDrive
910
+ \`.rmfmail/\` folder).
911
+ - This document is **app-internal** \u2014 it ships with each release and
912
+ documents the editor as it currently exists in the version you're
913
+ running. It is not deployed to your user folder.
914
+ `;
915
+ }
916
+ });
917
+
918
+ // client/compose/editor.js
919
+ function looksLikeUrl(s) {
920
+ const t = s.trim();
921
+ if (!t)
922
+ return false;
923
+ if (/^(https?|mailto|tel):/i.test(t))
924
+ return true;
925
+ return /^[\w-]+(\.[\w-]+)+(\/\S*)?$/.test(t);
926
+ }
927
+ function normalizeUrl(s) {
928
+ const t = s.trim();
929
+ if (!t)
930
+ return t;
931
+ if (/^(https?|mailto|tel):/i.test(t))
932
+ return t;
933
+ if (/^[\w.+-]+@[\w-]+(\.[\w-]+)+$/.test(t))
934
+ return `mailto:${t}`;
935
+ return `https://${t}`;
936
+ }
937
+ function openLinkDialog(initialText, initialUrl) {
938
+ return new Promise((resolve) => {
939
+ const backdrop = document.createElement("div");
940
+ backdrop.className = "mailx-modal-backdrop";
941
+ const panel = document.createElement("div");
942
+ panel.className = "mailx-modal";
943
+ panel.innerHTML = `
944
+ <div class="mailx-modal-title">Edit link</div>
945
+ <label class="mailx-modal-label">Text
946
+ <input type="text" class="mailx-modal-input" id="mailx-link-text">
947
+ </label>
948
+ <label class="mailx-modal-label">URL
949
+ <input type="text" class="mailx-modal-input" id="mailx-link-url" spellcheck="false" autocomplete="off">
950
+ </label>
951
+ <div class="mailx-modal-buttons">
952
+ <button type="button" class="mailx-modal-btn" data-action="remove">Remove link</button>
953
+ <span class="mailx-modal-spacer"></span>
954
+ <button type="button" class="mailx-modal-btn" data-action="cancel">Cancel</button>
955
+ <button type="button" class="mailx-modal-btn mailx-modal-btn-primary" data-action="ok">OK</button>
956
+ </div>`;
957
+ backdrop.appendChild(panel);
958
+ document.body.appendChild(backdrop);
959
+ const textInput = panel.querySelector("#mailx-link-text");
960
+ const urlInput = panel.querySelector("#mailx-link-url");
961
+ textInput.value = initialText;
962
+ urlInput.value = initialUrl;
963
+ const close = (result) => {
964
+ backdrop.remove();
965
+ document.removeEventListener("keydown", onKey, true);
966
+ resolve(result);
967
+ };
968
+ const commit = () => close({ text: textInput.value, url: normalizeUrl(urlInput.value) });
969
+ const onKey = (e) => {
970
+ if (e.key === "Escape") {
971
+ e.stopPropagation();
972
+ e.preventDefault();
973
+ close(null);
974
+ } else if (e.key === "Enter") {
975
+ e.stopPropagation();
976
+ e.preventDefault();
977
+ commit();
978
+ }
979
+ };
980
+ document.addEventListener("keydown", onKey, true);
981
+ panel.querySelectorAll(".mailx-modal-btn").forEach((btn) => {
982
+ btn.addEventListener("click", () => {
983
+ const action = btn.dataset.action;
984
+ if (action === "cancel")
985
+ close(null);
986
+ else if (action === "remove")
987
+ close({ text: textInput.value, url: "", remove: true });
988
+ else
989
+ commit();
990
+ });
991
+ });
992
+ backdrop.addEventListener("mousedown", (e) => {
993
+ if (e.target === backdrop)
994
+ close(null);
995
+ });
996
+ (initialText ? urlInput : textInput).focus();
997
+ (initialText ? urlInput : textInput).select();
998
+ });
999
+ }
1000
+ function createQuillEditor(container2) {
1001
+ const openLinkForRange = async (quill, range) => {
1002
+ if (!range)
1003
+ return;
1004
+ let linkRange = range;
1005
+ const format = quill.getFormat(range);
1006
+ if (range.length === 0 && format.link) {
1007
+ const [leaf, offset] = quill.getLeaf(range.index);
1008
+ if (leaf) {
1009
+ const text = quill.getText();
1010
+ let start = range.index, end = range.index;
1011
+ while (start > 0 && quill.getFormat(start - 1, 1).link === format.link)
1012
+ start--;
1013
+ while (end < text.length - 1 && quill.getFormat(end, 1).link === format.link)
1014
+ end++;
1015
+ linkRange = { index: start, length: end - start };
1016
+ }
1017
+ }
1018
+ const currentText = linkRange.length ? quill.getText(linkRange.index, linkRange.length).replace(/\n$/, "") : "";
1019
+ const currentUrl = format.link || "";
1020
+ const result = await openLinkDialog(currentText, currentUrl);
1021
+ if (!result)
1022
+ return;
1023
+ if (result.remove) {
1024
+ if (linkRange.length)
1025
+ quill.formatText(linkRange.index, linkRange.length, "link", false);
1026
+ return;
1027
+ }
1028
+ if (!result.url)
1029
+ return;
1030
+ const newText = result.text || result.url;
1031
+ if (linkRange.length) {
1032
+ quill.deleteText(linkRange.index, linkRange.length);
1033
+ quill.insertText(linkRange.index, newText, { link: result.url });
1034
+ quill.setSelection(linkRange.index + newText.length, 0);
1035
+ } else {
1036
+ quill.insertText(linkRange.index, newText, { link: result.url });
1037
+ quill.setSelection(linkRange.index + newText.length, 0);
1038
+ }
1039
+ };
1040
+ const extraBindings = {
1041
+ insertLink: {
1042
+ key: "K",
1043
+ shortKey: true,
1044
+ handler: function(range) {
1045
+ openLinkForRange(this.quill, range);
1046
+ }
1047
+ },
1048
+ removeLink: {
1049
+ key: "K",
1050
+ shortKey: true,
1051
+ shiftKey: true,
1052
+ handler: function() {
1053
+ this.quill.format("link", false);
1054
+ }
1055
+ },
1056
+ strike: {
1057
+ key: "X",
1058
+ shortKey: true,
1059
+ shiftKey: true,
1060
+ handler: function(range) {
1061
+ if (!range)
1062
+ return true;
1063
+ const cur = this.quill.getFormat(range).strike;
1064
+ this.quill.format("strike", !cur);
1065
+ }
1066
+ },
1067
+ orderedList: {
1068
+ key: "7",
1069
+ shortKey: true,
1070
+ shiftKey: true,
1071
+ handler: function() {
1072
+ this.quill.format("list", "ordered");
1073
+ }
1074
+ },
1075
+ bulletList: {
1076
+ key: "8",
1077
+ shortKey: true,
1078
+ shiftKey: true,
1079
+ handler: function() {
1080
+ this.quill.format("list", "bullet");
1081
+ }
1082
+ },
1083
+ indent: {
1084
+ key: "]",
1085
+ shortKey: true,
1086
+ handler: function(range, context) {
1087
+ this.quill.format("indent", (context.format.indent || 0) + 1);
1088
+ }
1089
+ },
1090
+ outdent: {
1091
+ key: "[",
1092
+ shortKey: true,
1093
+ handler: function(range, context) {
1094
+ this.quill.format("indent", Math.max(0, (context.format.indent || 0) - 1));
1095
+ }
1096
+ },
1097
+ color: {
1098
+ key: "C",
1099
+ shortKey: true,
1100
+ shiftKey: true,
1101
+ handler: function(range) {
1102
+ if (!range)
1103
+ return true;
1104
+ const current = this.quill.getFormat(range).color || "";
1105
+ const color = prompt("Text color (name or #hex, blank to clear):", current);
1106
+ if (color === null)
1107
+ return;
1108
+ this.quill.format("color", color || false);
1109
+ }
1110
+ },
1111
+ clearFormat: {
1112
+ key: "\\",
1113
+ shortKey: true,
1114
+ handler: function(range) {
1115
+ if (!range)
1116
+ return true;
1117
+ this.quill.removeFormat(range.index, range.length || 0);
1118
+ }
1119
+ }
1120
+ };
1121
+ const q = new Quill(container2, {
1122
+ theme: "snow",
1123
+ placeholder: "Write your message...",
1124
+ modules: {
1125
+ toolbar: [
1126
+ [{ font: [] }, { size: ["small", false, "large", "huge"] }],
1127
+ [{ header: [1, 2, 3, false] }],
1128
+ ["bold", "italic", "underline", "strike"],
1129
+ [{ color: [] }, { background: [] }],
1130
+ [{ list: "ordered" }, { list: "bullet" }],
1131
+ [{ align: [] }],
1132
+ ["blockquote", "link", "image"],
1133
+ ["clean"]
1134
+ ],
1135
+ keyboard: { bindings: extraBindings }
1136
+ }
1137
+ });
1138
+ document.querySelectorAll(".ql-toolbar button, .ql-toolbar select, .ql-toolbar .ql-picker-label").forEach((el) => el.setAttribute("tabindex", "-1"));
1139
+ const applySpellcheck = () => {
1140
+ if (q.root.getAttribute("spellcheck") !== "true")
1141
+ q.root.setAttribute("spellcheck", "true");
1142
+ if (q.root.getAttribute("autocorrect") !== "on")
1143
+ q.root.setAttribute("autocorrect", "on");
1144
+ if (q.root.getAttribute("autocapitalize") !== "on")
1145
+ q.root.setAttribute("autocapitalize", "on");
1146
+ };
1147
+ applySpellcheck();
1148
+ requestAnimationFrame(applySpellcheck);
1149
+ setTimeout(applySpellcheck, 100);
1150
+ const spellObs = new MutationObserver(() => applySpellcheck());
1151
+ spellObs.observe(q.root, { attributes: true, attributeFilter: ["spellcheck", "autocorrect", "autocapitalize"] });
1152
+ const toolbar = q.getModule("toolbar");
1153
+ toolbar?.addHandler("link", function() {
1154
+ openLinkForRange(q, q.getSelection() || { index: q.getLength() - 1, length: 0 });
1155
+ });
1156
+ q.root.addEventListener("contextmenu", async (e) => {
1157
+ try {
1158
+ const sel = q.getSelection();
1159
+ if (!sel || sel.length === 0)
1160
+ return;
1161
+ const settingsRaw = localStorage.getItem("mailx-ai-proofread-enabled");
1162
+ if (settingsRaw !== "true")
1163
+ return;
1164
+ const text = q.getText(sel.index, sel.length);
1165
+ if (!text.trim())
1166
+ return;
1167
+ e.preventDefault();
1168
+ const menu = document.createElement("div");
1169
+ menu.style.cssText = `position:fixed;z-index:2500;background:var(--color-bg);color:var(--color-text);border:1px solid var(--color-border);border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,0.2);padding:4px 0;font-size:13px;min-width:160px;`;
1170
+ menu.style.left = `${e.clientX}px`;
1171
+ menu.style.top = `${e.clientY}px`;
1172
+ const item = document.createElement("div");
1173
+ item.textContent = "Proofread selection";
1174
+ item.style.cssText = `padding:6px 12px;cursor:pointer;`;
1175
+ item.addEventListener("mouseenter", () => item.style.background = "var(--color-bg-hover)");
1176
+ item.addEventListener("mouseleave", () => item.style.background = "");
1177
+ item.addEventListener("click", async () => {
1178
+ menu.remove();
1179
+ try {
1180
+ const { aiTransform: aiTransform2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
1181
+ const r = await aiTransform2({ action: "proofread", text });
1182
+ if (r?.text && r.text !== text) {
1183
+ q.deleteText(sel.index, sel.length);
1184
+ q.insertText(sel.index, r.text);
1185
+ q.setSelection(sel.index, r.text.length);
1186
+ } else if (r?.reason) {
1187
+ alert(`Proofread: ${r.reason}`);
1188
+ }
1189
+ } catch (err) {
1190
+ alert(`Proofread failed: ${err?.message || err}`);
1191
+ }
1192
+ });
1193
+ menu.appendChild(item);
1194
+ document.body.appendChild(menu);
1195
+ const dismiss2 = () => {
1196
+ menu.remove();
1197
+ document.removeEventListener("mousedown", dismiss2);
1198
+ };
1199
+ setTimeout(() => document.addEventListener("mousedown", dismiss2), 0);
1200
+ } catch {
1201
+ }
1202
+ });
1203
+ q.root.addEventListener("paste", (e) => {
1204
+ const cb = e.clipboardData;
1205
+ if (!cb)
1206
+ return;
1207
+ const consume = () => {
1208
+ e.preventDefault();
1209
+ e.stopImmediatePropagation();
1210
+ };
1211
+ for (const item of Array.from(cb.items)) {
1212
+ if (item.kind === "file" && item.type.startsWith("image/")) {
1213
+ const file = item.getAsFile();
1214
+ if (!file)
1215
+ continue;
1216
+ consume();
1217
+ const reader = new FileReader();
1218
+ reader.onload = () => {
1219
+ const dataUrl = String(reader.result || "");
1220
+ const range = q.getSelection(true) || { index: q.getLength(), length: 0 };
1221
+ q.insertEmbed(range.index, "image", dataUrl);
1222
+ q.setSelection(range.index + 1, 0);
1223
+ };
1224
+ reader.readAsDataURL(file);
1225
+ return;
1226
+ }
1227
+ }
1228
+ const html = cb.getData("text/html");
1229
+ const plain = cb.getData("text/plain");
1230
+ if (html) {
1231
+ try {
1232
+ const tmp = document.createElement("div");
1233
+ tmp.innerHTML = html;
1234
+ const root = tmp.querySelector("body") || tmp;
1235
+ const meaningful = Array.from(root.childNodes).filter((n) => {
1236
+ if (n.nodeType === Node.TEXT_NODE)
1237
+ return (n.textContent || "").trim().length > 0;
1238
+ if (n.nodeType === Node.ELEMENT_NODE) {
1239
+ const tag = n.tagName.toLowerCase();
1240
+ return tag !== "meta" && tag !== "style" && tag !== "script";
1241
+ }
1242
+ return false;
1243
+ });
1244
+ if (meaningful.length === 1 && meaningful[0].tagName?.toLowerCase() === "a") {
1245
+ const a = meaningful[0];
1246
+ const href = a.getAttribute("href") || "";
1247
+ const text = (a.textContent || "").trim();
1248
+ if (href && text) {
1249
+ consume();
1250
+ const range = q.getSelection(true);
1251
+ if (!range)
1252
+ return;
1253
+ if (range.length > 0) {
1254
+ q.deleteText(range.index, range.length);
1255
+ }
1256
+ q.insertText(range.index, text, { link: href });
1257
+ q.setSelection(range.index + text.length, 0);
1258
+ return;
1259
+ }
1260
+ }
1261
+ const textOnly = (root.textContent || "").trim();
1262
+ if (textOnly && looksLikeUrl(textOnly) && !root.querySelector("a")) {
1263
+ consume();
1264
+ const range = q.getSelection(true);
1265
+ if (!range)
1266
+ return;
1267
+ const url = normalizeUrl(textOnly);
1268
+ if (range.length > 0) {
1269
+ q.formatText(range.index, range.length, "link", url);
1270
+ q.setSelection(range.index + range.length, 0);
1271
+ } else {
1272
+ q.insertText(range.index, textOnly, { link: url });
1273
+ q.setSelection(range.index + textOnly.length, 0);
1274
+ }
1275
+ return;
1276
+ }
1277
+ } catch {
1278
+ }
1279
+ return;
1280
+ }
1281
+ if (plain && looksLikeUrl(plain)) {
1282
+ consume();
1283
+ const range = q.getSelection(true);
1284
+ if (!range)
1285
+ return;
1286
+ const url = normalizeUrl(plain);
1287
+ if (range.length > 0) {
1288
+ q.formatText(range.index, range.length, "link", url);
1289
+ q.setSelection(range.index + range.length, 0);
1290
+ } else {
1291
+ q.insertText(range.index, plain.trim(), { link: url });
1292
+ q.setSelection(range.index + plain.trim().length, 0);
1293
+ }
1294
+ }
1295
+ }, true);
1296
+ let hoverTip = null;
1297
+ q.root.addEventListener("mouseover", (e) => {
1298
+ const a = e.target.closest("a[href]");
1299
+ if (!a)
1300
+ return;
1301
+ if (hoverTip)
1302
+ hoverTip.remove();
1303
+ hoverTip = document.createElement("div");
1304
+ hoverTip.className = "mailx-link-hover";
1305
+ hoverTip.textContent = a.getAttribute("href") || "";
1306
+ document.body.appendChild(hoverTip);
1307
+ const rect = a.getBoundingClientRect();
1308
+ hoverTip.style.left = `${Math.max(8, rect.left)}px`;
1309
+ hoverTip.style.top = `${rect.bottom + 4}px`;
1310
+ });
1311
+ q.root.addEventListener("mouseout", (e) => {
1312
+ const to = e.relatedTarget;
1313
+ if (to && to.closest("a[href]"))
1314
+ return;
1315
+ if (hoverTip) {
1316
+ hoverTip.remove();
1317
+ hoverTip = null;
1318
+ }
1319
+ });
1320
+ return {
1321
+ setHtml(html) {
1322
+ q.clipboard.dangerouslyPasteHTML(html);
1323
+ },
1324
+ getHtml() {
1325
+ return q.root.innerHTML;
1326
+ },
1327
+ getText() {
1328
+ return q.getText();
1329
+ },
1330
+ focus() {
1331
+ q.focus();
1332
+ },
1333
+ setCursor(pos) {
1334
+ q.setSelection(pos, 0);
1335
+ },
1336
+ root: q.root,
1337
+ getScrollContainer() {
1338
+ return q.root;
1339
+ },
1340
+ onContentChange(handler) {
1341
+ q.on("text-change", handler);
1342
+ },
1343
+ onKeyDown(handler) {
1344
+ q.root.addEventListener("keydown", handler);
1345
+ },
1346
+ insertTextAtCursor(text) {
1347
+ const sel = q.getSelection();
1348
+ if (sel)
1349
+ q.insertText(sel.index, text);
1350
+ }
1351
+ };
1352
+ }
1353
+ async function createTiptapEditor(container2) {
1354
+ const { Editor } = window.tiptapCore;
1355
+ const { StarterKit } = window.tiptapStarterKit;
1356
+ const { Link } = window.tiptapExtensionLink;
1357
+ const { Image } = window.tiptapExtensionImage;
1358
+ const { Underline } = window.tiptapExtensionUnderline;
1359
+ const { Placeholder } = window.tiptapExtensionPlaceholder;
1360
+ const toolbar = document.createElement("div");
1361
+ toolbar.className = "tt-toolbar";
1362
+ toolbar.innerHTML = `
1363
+ <select class="tt-heading" tabindex="-1">
1364
+ <option value="p">Normal</option>
1365
+ <option value="1">Heading 1</option>
1366
+ <option value="2">Heading 2</option>
1367
+ <option value="3">Heading 3</option>
1368
+ </select>
1369
+ <button class="tt-btn" data-cmd="bold" title="Bold" tabindex="-1"><b>B</b></button>
1370
+ <button class="tt-btn" data-cmd="italic" title="Italic" tabindex="-1"><i>I</i></button>
1371
+ <button class="tt-btn" data-cmd="underline" title="Underline" tabindex="-1"><u>U</u></button>
1372
+ <button class="tt-btn" data-cmd="strike" title="Strikethrough" tabindex="-1"><s>S</s></button>
1373
+ <button class="tt-btn" data-cmd="bulletList" title="Bullet list" tabindex="-1">&#8226;</button>
1374
+ <button class="tt-btn" data-cmd="orderedList" title="Ordered list" tabindex="-1">1.</button>
1375
+ <button class="tt-btn" data-cmd="blockquote" title="Blockquote" tabindex="-1">&ldquo;</button>
1376
+ <button class="tt-btn" data-cmd="link" title="Link" tabindex="-1">&#128279;</button>
1377
+ <button class="tt-btn" data-cmd="clearFormat" title="Clear formatting" tabindex="-1">&#8999;</button>
1378
+ `;
1379
+ const content = document.createElement("div");
1380
+ content.className = "tt-content";
1381
+ container2.appendChild(toolbar);
1382
+ container2.appendChild(content);
1383
+ const ed = new Editor({
1384
+ element: content,
1385
+ extensions: [
1386
+ StarterKit,
1387
+ Link.configure({ openOnClick: false }),
1388
+ Image,
1389
+ Underline,
1390
+ Placeholder.configure({ placeholder: "Write your message..." })
1391
+ ],
1392
+ content: ""
1393
+ });
1394
+ toolbar.querySelectorAll(".tt-btn").forEach((btn) => {
1395
+ btn.addEventListener("mousedown", (e) => {
1396
+ e.preventDefault();
1397
+ const cmd = btn.dataset.cmd;
1398
+ switch (cmd) {
1399
+ case "bold":
1400
+ ed.chain().focus().toggleBold().run();
1401
+ break;
1402
+ case "italic":
1403
+ ed.chain().focus().toggleItalic().run();
1404
+ break;
1405
+ case "underline":
1406
+ ed.chain().focus().toggleUnderline().run();
1407
+ break;
1408
+ case "strike":
1409
+ ed.chain().focus().toggleStrike().run();
1410
+ break;
1411
+ case "bulletList":
1412
+ ed.chain().focus().toggleBulletList().run();
1413
+ break;
1414
+ case "orderedList":
1415
+ ed.chain().focus().toggleOrderedList().run();
1416
+ break;
1417
+ case "blockquote":
1418
+ ed.chain().focus().toggleBlockquote().run();
1419
+ break;
1420
+ case "link": {
1421
+ const url = prompt("URL:");
1422
+ if (url)
1423
+ ed.chain().focus().setLink({ href: url }).run();
1424
+ break;
1425
+ }
1426
+ case "clearFormat":
1427
+ ed.chain().focus().clearNodes().unsetAllMarks().run();
1428
+ break;
1429
+ }
1430
+ });
1431
+ });
1432
+ const headingSelect = toolbar.querySelector(".tt-heading");
1433
+ headingSelect?.addEventListener("change", () => {
1434
+ const val = headingSelect.value;
1435
+ if (val === "p")
1436
+ ed.chain().focus().setParagraph().run();
1437
+ else
1438
+ ed.chain().focus().toggleHeading({ level: parseInt(val) }).run();
1439
+ });
1440
+ content.addEventListener("keydown", (e) => {
1441
+ const mod = e.ctrlKey || e.metaKey;
1442
+ if (!mod)
1443
+ return;
1444
+ const k = e.key.toLowerCase();
1445
+ if (e.shiftKey && k === "7") {
1446
+ e.preventDefault();
1447
+ ed.chain().focus().toggleOrderedList().run();
1448
+ return;
1449
+ }
1450
+ if (e.shiftKey && k === "8") {
1451
+ e.preventDefault();
1452
+ ed.chain().focus().toggleBulletList().run();
1453
+ return;
1454
+ }
1455
+ if (e.shiftKey && k === "x") {
1456
+ e.preventDefault();
1457
+ ed.chain().focus().toggleStrike().run();
1458
+ return;
1459
+ }
1460
+ if (e.shiftKey && k === "k") {
1461
+ e.preventDefault();
1462
+ ed.chain().focus().unsetLink().run();
1463
+ return;
1464
+ }
1465
+ if (!e.shiftKey && k === "k") {
1466
+ e.preventDefault();
1467
+ const url = prompt("URL:");
1468
+ if (url)
1469
+ ed.chain().focus().setLink({ href: url }).run();
1470
+ return;
1471
+ }
1472
+ if (k === "\\") {
1473
+ e.preventDefault();
1474
+ ed.chain().focus().clearNodes().unsetAllMarks().run();
1475
+ return;
1476
+ }
1477
+ });
1478
+ const editorEl = content.querySelector(".tiptap") || content;
1479
+ return {
1480
+ setHtml(html) {
1481
+ ed.commands.setContent(html);
1482
+ },
1483
+ getHtml() {
1484
+ return ed.getHTML();
1485
+ },
1486
+ getText() {
1487
+ return ed.getText();
1488
+ },
1489
+ focus() {
1490
+ ed.commands.focus("start");
1491
+ },
1492
+ setCursor(pos) {
1493
+ ed.commands.focus("start");
1494
+ },
1495
+ root: editorEl,
1496
+ getScrollContainer() {
1497
+ return editorEl;
1498
+ },
1499
+ onContentChange(handler) {
1500
+ ed.on("update", handler);
1501
+ },
1502
+ onKeyDown(handler) {
1503
+ editorEl.addEventListener("keydown", handler);
1504
+ },
1505
+ insertTextAtCursor(text) {
1506
+ ed.commands.insertContent(text);
1507
+ }
1508
+ };
1509
+ }
1510
+ async function createEditor(container2, type) {
1511
+ if (type === "tiptap") {
1512
+ return createTiptapEditor(container2);
1513
+ }
1514
+ if (type === "tinymce") {
1515
+ return createTinyMceEditor2(container2);
1516
+ }
1517
+ return createQuillEditor(container2);
1518
+ }
1519
+ var DEFAULT_TINYMCE_CDN = "../lib/tinymce/tinymce.min.js";
1520
+ async function createTinyMceEditor2(container2) {
1521
+ let cdnUrl = DEFAULT_TINYMCE_CDN;
1522
+ let apiKey;
1523
+ try {
1524
+ cdnUrl = localStorage.getItem("mailx-tinymce-cdn") || DEFAULT_TINYMCE_CDN;
1525
+ apiKey = localStorage.getItem("mailx-tinymce-apikey") || void 0;
1526
+ } catch {
1527
+ }
1528
+ const m = await Promise.resolve().then(() => (init_rmf_tiny(), rmf_tiny_exports));
1529
+ const ed = await m.createTinyMceEditor(container2, { cdnUrl, apiKey });
1530
+ return ed;
1531
+ }
1532
+
1533
+ // client/compose/compose.ts
1534
+ init_api_client();
1535
+
1536
+ // client/components/context-menu.js
1537
+ var activeMenu = null;
1538
+ var dismissListener = null;
1539
+ var escapeListener = null;
1540
+ function closeContextMenu() {
1541
+ if (activeMenu) {
1542
+ activeMenu.remove();
1543
+ activeMenu = null;
1544
+ }
1545
+ if (dismissListener) {
1546
+ document.removeEventListener("pointerdown", dismissListener, true);
1547
+ dismissListener = null;
1548
+ }
1549
+ if (escapeListener) {
1550
+ document.removeEventListener("keydown", escapeListener, true);
1551
+ escapeListener = null;
1552
+ }
1553
+ }
1554
+ function showContextMenu(x, y, items) {
1555
+ closeContextMenu();
1556
+ const menu = document.createElement("div");
1557
+ menu.className = "ctx-menu";
1558
+ for (const item of items) {
1559
+ if (item.separator) {
1560
+ const sep = document.createElement("div");
1561
+ sep.className = "ctx-sep";
1562
+ menu.appendChild(sep);
1563
+ continue;
1564
+ }
1565
+ const el = document.createElement("div");
1566
+ el.className = "ctx-item" + (item.disabled ? " ctx-disabled" : "");
1567
+ el.textContent = item.label;
1568
+ if (!item.disabled) {
1569
+ el.addEventListener("click", () => {
1570
+ closeContextMenu();
1571
+ item.action();
1572
+ });
1573
+ }
1574
+ menu.appendChild(el);
1575
+ }
1576
+ menu.style.left = `${x}px`;
1577
+ menu.style.top = `${y}px`;
1578
+ document.body.appendChild(menu);
1579
+ const rect = menu.getBoundingClientRect();
1580
+ if (rect.right > window.innerWidth)
1581
+ menu.style.left = `${x - rect.width}px`;
1582
+ if (rect.bottom > window.innerHeight)
1583
+ menu.style.top = `${y - rect.height}px`;
1584
+ activeMenu = menu;
1585
+ requestAnimationFrame(() => {
1586
+ dismissListener = (e) => {
1587
+ if (activeMenu && !activeMenu.contains(e.target)) {
1588
+ closeContextMenu();
1589
+ }
1590
+ };
1591
+ document.addEventListener("pointerdown", dismissListener, true);
1592
+ escapeListener = (e) => {
1593
+ if (e.key === "Escape") {
1594
+ e.preventDefault();
1595
+ e.stopPropagation();
1596
+ closeContextMenu();
1597
+ }
1598
+ };
1599
+ document.addEventListener("keydown", escapeListener, true);
1600
+ });
1601
+ }
1602
+ document.addEventListener("scroll", closeContextMenu, true);
1603
+ document.addEventListener("contextmenu", () => {
1604
+ });
1605
+
1606
+ // client/compose/compose.ts
1607
+ logClientEvent("compose-module-loaded", { href: location.href, version: window.mailxVersion || "?" });
1608
+ function closeCompose() {
1609
+ logClientEvent("compose-close");
1610
+ try {
1611
+ parent.postMessage({ type: "mailx-compose-close" }, "*");
1612
+ } catch {
1613
+ }
1614
+ try {
1615
+ window.close();
1616
+ } catch {
1617
+ }
1618
+ }
1619
+ function loadScript(src) {
1620
+ return new Promise((resolve, reject) => {
1621
+ const s = document.createElement("script");
1622
+ s.src = src;
1623
+ s.onload = () => resolve();
1624
+ s.onerror = () => reject(new Error(`Failed to load ${src}`));
1625
+ document.head.appendChild(s);
1626
+ });
1627
+ }
1628
+ function loadCSS(href) {
1629
+ const link = document.createElement("link");
1630
+ link.rel = "stylesheet";
1631
+ link.href = href;
1632
+ document.head.appendChild(link);
1633
+ }
1634
+ async function loadEditorAssets(type) {
1635
+ if (type === "tiptap") {
1636
+ const cdn = "https://cdn.jsdelivr.net/npm";
1637
+ await loadScript(`${cdn}/@tiptap/core@2/dist/index.umd.js`);
1638
+ await Promise.all([
1639
+ loadScript(`${cdn}/@tiptap/starter-kit@2/dist/index.umd.js`),
1640
+ loadScript(`${cdn}/@tiptap/extension-link@2/dist/index.umd.js`),
1641
+ loadScript(`${cdn}/@tiptap/extension-image@2/dist/index.umd.js`),
1642
+ loadScript(`${cdn}/@tiptap/extension-underline@2/dist/index.umd.js`),
1643
+ loadScript(`${cdn}/@tiptap/extension-placeholder@2/dist/index.umd.js`)
1644
+ ]);
1645
+ } else {
1646
+ loadCSS("https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css");
1647
+ await loadScript("https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js");
1648
+ }
1649
+ }
1650
+ var editorType = "quill";
1651
+ var appSettings = null;
1652
+ try {
1653
+ const cached = localStorage.getItem("mailx-editor-type");
1654
+ if (cached === "tiptap" || cached === "quill" || cached === "tinymce") editorType = cached;
1655
+ } catch {
1656
+ }
1657
+ (async () => {
1658
+ try {
1659
+ appSettings = await getSettings();
1660
+ const settingValue = appSettings?.ui?.editor;
1661
+ const next = settingValue === "tiptap" ? "tiptap" : settingValue === "tinymce" ? "tinymce" : "quill";
1662
+ try {
1663
+ localStorage.setItem("mailx-editor-type", next);
1664
+ } catch {
1665
+ }
1666
+ } catch {
1667
+ }
1668
+ })();
1669
+ var editor;
1670
+ var container = document.getElementById("compose-editor");
1671
+ function setActiveEditorBadge(type) {
1672
+ const el = document.getElementById("compose-editor-badge");
1673
+ if (!el) return;
1674
+ const labels = {
1675
+ quill: "Quill",
1676
+ tiptap: "tiptap",
1677
+ tinymce: "TinyMCE",
1678
+ fallback: "plain (fallback)"
1679
+ };
1680
+ const docs = {
1681
+ quill: "https://quilljs.com/docs/quickstart",
1682
+ tiptap: "https://tiptap.dev/docs/editor/introduction",
1683
+ tinymce: "https://www.tiny.cloud/docs/tinymce/6/",
1684
+ fallback: "https://github.com/BobFrankston/mailx/blob/master/app/docs/editor.md"
1685
+ };
1686
+ el.textContent = labels[type] || type;
1687
+ el.dataset.editor = type;
1688
+ const url = docs[type];
1689
+ if (url) {
1690
+ el.style.cursor = "pointer";
1691
+ el.title = `Open ${labels[type]} documentation`;
1692
+ el.onclick = () => {
1693
+ const api = window.mailxapi;
1694
+ if (api?.openExternal) api.openExternal(url);
1695
+ else window.open(url, "_blank", "noopener,noreferrer");
1696
+ };
1697
+ } else {
1698
+ el.style.cursor = "";
1699
+ el.title = "";
1700
+ el.onclick = null;
1701
+ }
1702
+ }
1703
+ async function tryEditor(type) {
1704
+ try {
1705
+ if (type !== "tinymce") await loadEditorAssets(type);
1706
+ } catch (e) {
1707
+ logClientEvent("compose-editor-assets-failed", { type, error: String(e?.message || e) });
1708
+ return null;
1709
+ }
1710
+ container.classList.remove("editor-tiptap", "editor-quill", "editor-tinymce");
1711
+ container.classList.add(`editor-${type}`);
1712
+ try {
1713
+ return await createEditor(container, type);
1714
+ } catch (e) {
1715
+ logClientEvent("compose-editor-create-failed", { type, error: String(e?.message || e) });
1716
+ return null;
1717
+ }
1718
+ }
1719
+ var activeEditorType = editorType;
1720
+ editor = await tryEditor(editorType);
1721
+ if (!editor) {
1722
+ const fallbackType = editorType === "quill" ? "tiptap" : "quill";
1723
+ logClientEvent("compose-editor-fallback-other", { from: editorType, to: fallbackType });
1724
+ container.innerHTML = "";
1725
+ editor = await tryEditor(fallbackType);
1726
+ if (editor) {
1727
+ activeEditorType = fallbackType;
1728
+ setTimeout(() => showDraftStatus(`${editorType} editor unavailable \u2014 using ${fallbackType} instead.`, false), 0);
1729
+ }
1730
+ }
1731
+ if (!editor) {
1732
+ logClientEvent("compose-editor-create-failed-both", {});
1733
+ container.innerHTML = `<div class="compose-fallback-editor" contenteditable="true" style="border:1px solid #c00;padding:8px;min-height:200px;background:#fff" data-fallback="true"></div>`;
1734
+ const fallback = container.querySelector(".compose-fallback-editor");
1735
+ editor = {
1736
+ root: fallback,
1737
+ setHtml: (html) => {
1738
+ fallback.innerHTML = html;
1739
+ },
1740
+ getHtml: () => fallback.innerHTML,
1741
+ getText: () => fallback.innerText,
1742
+ focus: () => fallback.focus(),
1743
+ setCursor: () => {
1744
+ },
1745
+ getScrollContainer: () => fallback,
1746
+ onContentChange: (handler) => {
1747
+ fallback.addEventListener("input", handler);
1748
+ },
1749
+ onKeyDown: (handler) => {
1750
+ fallback.addEventListener("keydown", handler);
1751
+ },
1752
+ insertTextAtCursor: (text) => {
1753
+ const sel = window.getSelection();
1754
+ if (sel && sel.rangeCount > 0) {
1755
+ const range = sel.getRangeAt(0);
1756
+ range.deleteContents();
1757
+ range.insertNode(document.createTextNode(text));
1758
+ } else {
1759
+ fallback.append(document.createTextNode(text));
1760
+ }
1761
+ }
1762
+ };
1763
+ activeEditorType = "fallback";
1764
+ setTimeout(() => showDraftStatus(`Both editors failed to load. Plain-text fallback in use. Check the log; CDN may be unreachable.`, true), 0);
1765
+ }
1766
+ setActiveEditorBadge(activeEditorType);
1767
+ (() => {
1768
+ const STORAGE_KEY = "mailx.compose.zoom";
1769
+ const MIN = 0.5, MAX = 3, STEP = 0.1;
1770
+ let zoom = parseFloat(localStorage.getItem(STORAGE_KEY) || "1") || 1;
1771
+ const applyZoom = () => {
1772
+ container.style.fontSize = `${zoom}em`;
1773
+ localStorage.setItem(STORAGE_KEY, String(zoom));
1774
+ };
1775
+ applyZoom();
1776
+ container.addEventListener("wheel", (e) => {
1777
+ if (!e.ctrlKey) return;
1778
+ e.preventDefault();
1779
+ const delta = e.deltaY < 0 ? STEP : -STEP;
1780
+ zoom = Math.min(MAX, Math.max(MIN, Math.round((zoom + delta) * 10) / 10));
1781
+ applyZoom();
1782
+ }, { passive: false });
1783
+ document.addEventListener("keydown", (e) => {
1784
+ if (!(e.ctrlKey || e.metaKey)) return;
1785
+ if (e.key === "=" || e.key === "+") {
1786
+ zoom = Math.min(MAX, zoom + STEP);
1787
+ applyZoom();
1788
+ e.preventDefault();
1789
+ } else if (e.key === "-") {
1790
+ zoom = Math.max(MIN, zoom - STEP);
1791
+ applyZoom();
1792
+ e.preventDefault();
1793
+ } else if (e.key === "0") {
1794
+ zoom = 1;
1795
+ applyZoom();
1796
+ e.preventDefault();
1797
+ }
1798
+ });
1799
+ })();
1800
+ var fromInput = document.getElementById("compose-from-input");
1801
+ var fromOptions = document.getElementById("compose-from-options");
1802
+ var toInput = document.getElementById("compose-to");
1803
+ var ccInput = document.getElementById("compose-cc");
1804
+ var bccInput = document.getElementById("compose-bcc");
1805
+ var subjectInput = document.getElementById("compose-subject");
1806
+ function autoGrowAddrInput(el) {
1807
+ if (!el) return;
1808
+ el.style.height = "auto";
1809
+ const max = 8 * 22;
1810
+ el.style.height = Math.min(el.scrollHeight, max) + "px";
1811
+ if (el.scrollHeight > max) el.style.overflowY = "auto";
1812
+ else el.style.overflowY = "hidden";
1813
+ }
1814
+ for (const el of [toInput, ccInput, bccInput]) {
1815
+ el?.addEventListener("input", () => autoGrowAddrInput(el));
1816
+ el?.addEventListener("keydown", (e) => {
1817
+ if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
1818
+ e.preventDefault();
1819
+ const start = el.selectionStart || 0;
1820
+ const v = el.value;
1821
+ el.value = v.slice(0, start).replace(/[,\s]+$/, "") + ", " + v.slice(start).replace(/^[,\s]+/, "");
1822
+ el.selectionStart = el.selectionEnd = start + 2;
1823
+ autoGrowAddrInput(el);
1824
+ }
1825
+ });
1826
+ }
1827
+ var knownAccounts = [];
1828
+ if (appSettings?.autocomplete?.enabled && appSettings.autocomplete.provider !== "off") {
1829
+ Promise.resolve().then(() => (init_ghost_text(), ghost_text_exports)).then(({ initGhostText: initGhostText2 }) => {
1830
+ initGhostText2(editor, {
1831
+ getSubject: () => subjectInput.value,
1832
+ getTo: () => toInput.value
1833
+ }, { debounceMs: appSettings.autocomplete.debounceMs || 600 });
1834
+ }).catch(() => {
1835
+ });
1836
+ }
1837
+ function formatAccountFrom(acct) {
1838
+ return `${acct.name} <${acct.email}>`;
1839
+ }
1840
+ var FROM_HISTORY_KEY = "mailx-from-history";
1841
+ var FROM_HISTORY_MAX = 20;
1842
+ function loadFromHistory() {
1843
+ try {
1844
+ return JSON.parse(localStorage.getItem(FROM_HISTORY_KEY) || "[]");
1845
+ } catch {
1846
+ return [];
1847
+ }
1848
+ }
1849
+ function recordFromHistory(value) {
1850
+ const v = (value || "").trim();
1851
+ if (!v) return;
1852
+ try {
1853
+ const list = loadFromHistory().filter((x) => x !== v);
1854
+ list.unshift(v);
1855
+ localStorage.setItem(FROM_HISTORY_KEY, JSON.stringify(list.slice(0, FROM_HISTORY_MAX)));
1856
+ } catch {
1857
+ }
1858
+ }
1859
+ function populateFromOptions(accounts, selectedId) {
1860
+ knownAccounts = accounts;
1861
+ fromOptions.innerHTML = "";
1862
+ const seenValues = /* @__PURE__ */ new Set();
1863
+ for (const acct of accounts) {
1864
+ const opt = document.createElement("option");
1865
+ opt.value = formatAccountFrom(acct);
1866
+ const tag = acct.label || acct.name;
1867
+ opt.label = tag;
1868
+ fromOptions.appendChild(opt);
1869
+ seenValues.add(opt.value);
1870
+ }
1871
+ for (const value of loadFromHistory()) {
1872
+ if (seenValues.has(value)) continue;
1873
+ const opt = document.createElement("option");
1874
+ opt.value = value;
1875
+ opt.label = "(used before)";
1876
+ fromOptions.appendChild(opt);
1877
+ }
1878
+ if (!fromInput.value) {
1879
+ const selected = selectedId && accounts.find((a) => a.id === selectedId) || accounts.find((a) => a.defaultSend) || accounts[0];
1880
+ if (selected) fromInput.value = formatAccountFrom(selected);
1881
+ }
1882
+ }
1883
+ function parseFromInput() {
1884
+ const raw = fromInput.value.trim();
1885
+ const match = raw.match(/^(.+?)\s*<(.+?)>$/);
1886
+ if (match) return { name: match[1].trim(), address: match[2].trim() };
1887
+ return { name: "", address: raw };
1888
+ }
1889
+ function getFromAccountId() {
1890
+ const { address } = parseFromInput();
1891
+ const lower = address.toLowerCase();
1892
+ const exact = knownAccounts.find((a) => a.email.toLowerCase() === lower);
1893
+ if (exact) return exact.id;
1894
+ const domain = lower.split("@")[1] || "";
1895
+ if (domain) {
1896
+ const sameDomain = knownAccounts.find((a) => a.email.toLowerCase().endsWith("@" + domain));
1897
+ if (sameDomain) return sameDomain.id;
1898
+ }
1899
+ const def = knownAccounts.find((a) => a.defaultSend) || knownAccounts[0];
1900
+ return def?.id || "";
1901
+ }
1902
+ function getFromAddress() {
1903
+ return fromInput.value.trim();
1904
+ }
1905
+ function showAutocompleteContextMenu(e, row) {
1906
+ showContextMenu(e.clientX, e.clientY, [
1907
+ {
1908
+ label: "Add to preferred\u2026",
1909
+ action: () => openAddToPreferredModal(row)
1910
+ },
1911
+ {
1912
+ label: "Never suggest this address",
1913
+ action: async () => {
1914
+ try {
1915
+ await addToDenylist(row.email);
1916
+ } catch (err) {
1917
+ alert(`Failed to add to denylist: ${err?.message || err}`);
1918
+ }
1919
+ }
1920
+ }
1921
+ ]);
1922
+ }
1923
+ function openAddToPreferredModal(prefill) {
1924
+ const overlay = document.createElement("div");
1925
+ overlay.className = "modal-overlay";
1926
+ overlay.innerHTML = `
1927
+ <div class="modal" role="dialog" aria-label="Add to preferred contacts">
1928
+ <h3>Add to preferred</h3>
1929
+ <p class="muted">Saved to <code>contacts.jsonc</code> on your shared drive.</p>
1930
+ <label>Name <input type="text" id="pf-name" /></label>
1931
+ <label>Email <input type="email" id="pf-email" /></label>
1932
+ <label>Source tag <input type="text" id="pf-source" placeholder="(optional \u2014 e.g. work, family)" /></label>
1933
+ <label>Organization <input type="text" id="pf-org" placeholder="(optional)" /></label>
1934
+ <div class="modal-actions">
1935
+ <button id="pf-cancel">Cancel</button>
1936
+ <button id="pf-save" class="primary">Save</button>
1937
+ </div>
1938
+ </div>
1939
+ `;
1940
+ document.body.appendChild(overlay);
1941
+ overlay.querySelector("#pf-name").value = prefill.name || "";
1942
+ overlay.querySelector("#pf-email").value = prefill.email || "";
1943
+ const sysSources = /* @__PURE__ */ new Set(["google", "discovered", "preferred", ""]);
1944
+ const initSource = sysSources.has(prefill.source || "") ? "" : prefill.source;
1945
+ overlay.querySelector("#pf-source").value = initSource;
1946
+ const close = () => overlay.remove();
1947
+ overlay.querySelector("#pf-cancel").addEventListener("click", close);
1948
+ overlay.addEventListener("click", (ev) => {
1949
+ if (ev.target === overlay) close();
1950
+ });
1951
+ overlay.querySelector("#pf-save").addEventListener("click", async () => {
1952
+ const name = overlay.querySelector("#pf-name").value.trim();
1953
+ const email = overlay.querySelector("#pf-email").value.trim();
1954
+ const source = overlay.querySelector("#pf-source").value.trim();
1955
+ const org = overlay.querySelector("#pf-org").value.trim();
1956
+ if (!email) {
1957
+ alert("Email is required.");
1958
+ return;
1959
+ }
1960
+ try {
1961
+ await addPreferredContact({ name, email, source, organization: org });
1962
+ close();
1963
+ } catch (err) {
1964
+ alert(`Failed to save: ${err?.message || err}`);
1965
+ }
1966
+ });
1967
+ overlay.querySelector("#pf-name").focus();
1968
+ }
1969
+ function setupAutocomplete(input) {
1970
+ let dropdown = null;
1971
+ let activeIndex = -1;
1972
+ let debounce;
1973
+ function closeDropdown() {
1974
+ if (dropdown) {
1975
+ dropdown.remove();
1976
+ dropdown = null;
1977
+ }
1978
+ activeIndex = -1;
1979
+ }
1980
+ function getLastToken() {
1981
+ const val = input.value;
1982
+ const lastComma = val.lastIndexOf(",");
1983
+ return val.substring(lastComma + 1).trim();
1984
+ }
1985
+ function replaceLastToken(replacement) {
1986
+ const val = input.value;
1987
+ const lastComma = val.lastIndexOf(",");
1988
+ const prefix = lastComma >= 0 ? val.substring(0, lastComma + 1) + " " : "";
1989
+ input.value = prefix + replacement + ", ";
1990
+ closeDropdown();
1991
+ input.focus();
1992
+ }
1993
+ input.addEventListener("input", () => {
1994
+ clearTimeout(debounce);
1995
+ const token = getLastToken();
1996
+ if (token.length < 1) {
1997
+ closeDropdown();
1998
+ return;
1999
+ }
2000
+ debounce = setTimeout(() => {
2001
+ requestAnimationFrame(async () => {
2002
+ try {
2003
+ const results = await searchContacts(token);
2004
+ if (results.length === 0) {
2005
+ closeDropdown();
2006
+ return;
2007
+ }
2008
+ closeDropdown();
2009
+ dropdown = document.createElement("div");
2010
+ dropdown.className = "ac-dropdown";
2011
+ activeIndex = 0;
2012
+ for (let i = 0; i < results.length; i++) {
2013
+ const item = document.createElement("div");
2014
+ item.className = `ac-item${i === 0 ? " ac-active" : ""}`;
2015
+ const nameEl = document.createElement("span");
2016
+ nameEl.className = "ac-item-name";
2017
+ nameEl.textContent = results[i].name || results[i].email;
2018
+ const emailEl = document.createElement("span");
2019
+ emailEl.className = "ac-item-email";
2020
+ emailEl.textContent = results[i].email;
2021
+ const sourceEl = document.createElement("span");
2022
+ sourceEl.className = "ac-item-source";
2023
+ sourceEl.textContent = results[i].source || "";
2024
+ item.appendChild(nameEl);
2025
+ if (results[i].name) item.appendChild(emailEl);
2026
+ if (results[i].source) item.appendChild(sourceEl);
2027
+ item.addEventListener("mousedown", (e) => {
2028
+ e.preventDefault();
2029
+ const display = results[i].name ? `${results[i].name} <${results[i].email}>` : results[i].email;
2030
+ replaceLastToken(display);
2031
+ });
2032
+ item.addEventListener("contextmenu", (e) => {
2033
+ e.preventDefault();
2034
+ e.stopPropagation();
2035
+ showAutocompleteContextMenu(e, results[i]);
2036
+ });
2037
+ dropdown.appendChild(item);
2038
+ }
2039
+ input.parentElement.appendChild(dropdown);
2040
+ requestAnimationFrame(() => {
2041
+ if (!dropdown) return;
2042
+ const rect = dropdown.getBoundingClientRect();
2043
+ const inputRect = input.getBoundingClientRect();
2044
+ const spaceBelow = window.innerHeight - inputRect.bottom;
2045
+ const spaceAbove = inputRect.top;
2046
+ if (rect.bottom > window.innerHeight && spaceAbove > spaceBelow) {
2047
+ dropdown.style.top = "auto";
2048
+ dropdown.style.bottom = "100%";
2049
+ }
2050
+ });
2051
+ } catch {
2052
+ }
2053
+ });
2054
+ }, 200);
2055
+ });
2056
+ input.addEventListener("keydown", (e) => {
2057
+ if (!dropdown) return;
2058
+ const items = dropdown.querySelectorAll(".ac-item");
2059
+ const ke = e;
2060
+ if (ke.key === "ArrowDown") {
2061
+ e.preventDefault();
2062
+ activeIndex = Math.min(activeIndex + 1, items.length - 1);
2063
+ items.forEach((el, i) => el.classList.toggle("ac-active", i === activeIndex));
2064
+ } else if (ke.key === "ArrowUp") {
2065
+ e.preventDefault();
2066
+ activeIndex = Math.max(activeIndex - 1, 0);
2067
+ items.forEach((el, i) => el.classList.toggle("ac-active", i === activeIndex));
2068
+ } else if (ke.key === "Tab" || ke.key === "Enter") {
2069
+ if (items.length > 0) {
2070
+ e.preventDefault();
2071
+ const idx = activeIndex >= 0 ? activeIndex : 0;
2072
+ items[idx].dispatchEvent(new MouseEvent("mousedown"));
2073
+ return;
2074
+ }
2075
+ } else if (ke.key === "Escape") {
2076
+ closeDropdown();
2077
+ }
2078
+ });
2079
+ input.addEventListener("blur", () => {
2080
+ setTimeout(closeDropdown, 150);
2081
+ });
2082
+ }
2083
+ setupAutocomplete(toInput);
2084
+ setupAutocomplete(ccInput);
2085
+ setupAutocomplete(bccInput);
2086
+ function formatAddrs(addrs) {
2087
+ return addrs.map((a) => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
2088
+ }
2089
+ function parseAddrs(s) {
2090
+ if (!s.trim()) return [];
2091
+ return s.split(",").map((p) => p.trim()).filter((p) => p.length > 0).map((part) => {
2092
+ const match = part.match(/^(.+?)\s*<(.+?)>$/);
2093
+ if (match) return { name: match[1].trim(), address: match[2].trim() };
2094
+ return { name: "", address: part };
2095
+ });
2096
+ }
2097
+ var GROUPS_LS_KEY = "mailx-groups-cache-v1";
2098
+ var _groupsCache = readGroupsFromLocalStorage();
2099
+ function readGroupsFromLocalStorage() {
2100
+ try {
2101
+ const raw = localStorage.getItem(GROUPS_LS_KEY);
2102
+ if (raw) return JSON.parse(raw) || {};
2103
+ } catch {
2104
+ }
2105
+ return {};
2106
+ }
2107
+ function writeGroupsToLocalStorage(groups) {
2108
+ try {
2109
+ localStorage.setItem(GROUPS_LS_KEY, JSON.stringify(groups));
2110
+ } catch {
2111
+ }
2112
+ }
2113
+ function refreshGroupsMapInBackground() {
2114
+ (async () => {
2115
+ try {
2116
+ const { readJsoncFile: readJsoncFile2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
2117
+ const r = await readJsoncFile2("contacts.jsonc");
2118
+ if (!r?.content) return;
2119
+ const stripped = r.content.replace(/^\s*\/\/.*$/gm, "").replace(/,(\s*[}\]])/g, "$1");
2120
+ const cfg = JSON.parse(stripped);
2121
+ const groups = cfg && typeof cfg.groups === "object" && cfg.groups ? cfg.groups : {};
2122
+ _groupsCache = groups;
2123
+ writeGroupsToLocalStorage(groups);
2124
+ } catch {
2125
+ }
2126
+ })();
2127
+ }
2128
+ refreshGroupsMapInBackground();
2129
+ function expandGroups(raw) {
2130
+ if (!raw.trim()) return raw;
2131
+ if (Object.keys(_groupsCache).length === 0) return raw;
2132
+ const groupsLc = {};
2133
+ for (const k of Object.keys(_groupsCache)) groupsLc[k.toLowerCase()] = _groupsCache[k];
2134
+ const tokens = raw.split(",").map((s) => s.trim()).filter(Boolean);
2135
+ const out = [];
2136
+ for (const tok of tokens) {
2137
+ const members = groupsLc[tok.toLowerCase()];
2138
+ if (members && Array.isArray(members)) out.push(...members);
2139
+ else out.push(tok);
2140
+ }
2141
+ return out.join(", ");
2142
+ }
2143
+ function applyInit(init) {
2144
+ populateFromOptions(init.accounts, init.accountId);
2145
+ if (init.fromAddress) {
2146
+ const account = init.accounts.find((a) => a.id === init.accountId);
2147
+ const displayName = account?.name || "";
2148
+ fromInput.value = displayName ? `${displayName} <${init.fromAddress}>` : init.fromAddress;
2149
+ }
2150
+ toInput.value = formatAddrs(init.to);
2151
+ ccInput.value = formatAddrs(init.cc);
2152
+ subjectInput.value = init.subject;
2153
+ autoGrowAddrInput(toInput);
2154
+ autoGrowAddrInput(ccInput);
2155
+ autoGrowAddrInput(bccInput);
2156
+ if (ccInput.value.trim()) {
2157
+ const ccRowEl = document.getElementById("compose-cc-row");
2158
+ const ccBtn = document.getElementById("btn-toggle-cc");
2159
+ if (ccRowEl) ccRowEl.hidden = false;
2160
+ if (ccBtn) ccBtn.classList.add("active");
2161
+ } else if (init.to && init.to.length === 1) {
2162
+ const firstEmail = init.to[0]?.address || "";
2163
+ if (firstEmail) {
2164
+ Promise.resolve().then(() => (init_api_client(), api_client_exports)).then(({ hasCcHistoryTo: hasCcHistoryTo2, hasBccHistoryTo: hasBccHistoryTo2 }) => {
2165
+ hasCcHistoryTo2(firstEmail).then((res) => {
2166
+ if (!res?.hasCc) return;
2167
+ const ccRowEl = document.getElementById("compose-cc-row");
2168
+ const ccBtn = document.getElementById("btn-toggle-cc");
2169
+ if (ccRowEl?.hidden && !ccInput.value) {
2170
+ ccRowEl.hidden = false;
2171
+ ccBtn?.classList.add("active");
2172
+ }
2173
+ }).catch(() => {
2174
+ });
2175
+ hasBccHistoryTo2(firstEmail).then((res) => {
2176
+ if (!res?.hasBcc) return;
2177
+ const bccRowEl = document.getElementById("compose-bcc-row");
2178
+ const bccBtn = document.getElementById("btn-toggle-bcc");
2179
+ if (bccRowEl?.hidden && !bccInput.value) {
2180
+ bccRowEl.hidden = false;
2181
+ bccBtn?.classList.add("active");
2182
+ }
2183
+ }).catch(() => {
2184
+ });
2185
+ });
2186
+ }
2187
+ }
2188
+ let bodyToRender = init.bodyHtml || "";
2189
+ const acct = init.accounts.find((a) => a.id === init.accountId);
2190
+ const isNew = init.mode !== "reply" && init.mode !== "replyAll" && init.mode !== "forward" && init.mode !== "draft" && !init.draftUid;
2191
+ const isReplyForward = init.mode === "reply" || init.mode === "replyAll" || init.mode === "forward";
2192
+ if (isNew && acct?.sig?.text) {
2193
+ const sigText = acct.sig.html ? acct.sig.text : escapeHtml(acct.sig.text).replace(/\n/g, "<br>");
2194
+ bodyToRender = `${bodyToRender}<br><br>-- <br>${sigText}`;
2195
+ } else if (acct?.signature && init.mode !== "draft" && !init.draftUid) {
2196
+ const sigBlock = `<br><br>--<br>${acct.signature}`;
2197
+ bodyToRender = isReplyForward ? `<br>${sigBlock}<br>${bodyToRender}` : `${bodyToRender}${sigBlock}`;
2198
+ }
2199
+ if (bodyToRender) {
2200
+ editor.setHtml(bodyToRender);
2201
+ editor.setCursor(0);
2202
+ }
2203
+ if (init.draftUid) {
2204
+ draftUid = init.draftUid;
2205
+ }
2206
+ setComposeTitle(init.subject || "");
2207
+ if (!toInput.value) toInput.focus();
2208
+ else if (!subjectInput.value) subjectInput.focus();
2209
+ else editor.focus();
2210
+ recordContentBaseline();
2211
+ }
2212
+ var composeDirty = false;
2213
+ function setComposeTitle(subject) {
2214
+ const base = subject ? `${subject} - Compose` : "Compose - mailx";
2215
+ document.title = composeDirty ? `\u2022 ${base}` : base;
2216
+ }
2217
+ function markComposeDirty() {
2218
+ if (composeDirty) return;
2219
+ composeDirty = true;
2220
+ setComposeTitle(subjectInput?.value || "");
2221
+ }
2222
+ function markComposeClean() {
2223
+ if (!composeDirty) return;
2224
+ composeDirty = false;
2225
+ setComposeTitle(subjectInput?.value || "");
2226
+ }
2227
+ var DRAFT_INPUT_DEBOUNCE_MS = 800;
2228
+ var DRAFT_INTERVAL_MS = 2e3;
2229
+ var draftUid = null;
2230
+ var draftId = null;
2231
+ var draftTimer = null;
2232
+ var draftDebounceTimer = null;
2233
+ var lastDraftContent = "";
2234
+ var baselineSnapshot = "";
2235
+ function getContentSnapshot() {
2236
+ return JSON.stringify({
2237
+ body: editor?.getHtml() || "",
2238
+ to: toInput?.value || "",
2239
+ cc: ccInput?.value || "",
2240
+ bcc: bccInput?.value || "",
2241
+ subject: subjectInput?.value || ""
2242
+ });
2243
+ }
2244
+ function recordContentBaseline() {
2245
+ baselineSnapshot = getContentSnapshot();
2246
+ lastDraftContent = baselineSnapshot;
2247
+ }
2248
+ function composeHasUserChanges() {
2249
+ return getContentSnapshot() !== baselineSnapshot;
2250
+ }
2251
+ var draftSaving = false;
2252
+ var draftSaveFailed = false;
2253
+ var attachments = [];
2254
+ function showDraftStatus(text, isError) {
2255
+ const status = document.getElementById("compose-status");
2256
+ if (!status) return;
2257
+ status.textContent = text;
2258
+ status.classList.toggle("compose-status-error", isError);
2259
+ }
2260
+ async function saveDraft2() {
2261
+ if (draftSaving) return;
2262
+ if (!draftUid && !draftId && !composeHasUserChanges()) return;
2263
+ const content = getContentSnapshot();
2264
+ if (content === lastDraftContent) return;
2265
+ window.__mailxSaveDraft = saveDraft2;
2266
+ lastDraftContent = content;
2267
+ draftSaving = true;
2268
+ try {
2269
+ const data = await saveDraft({
2270
+ accountId: getFromAccountId(),
2271
+ subject: subjectInput.value,
2272
+ bodyHtml: editor.getHtml(),
2273
+ bodyText: editor.getText(),
2274
+ to: toInput.value,
2275
+ cc: ccInput.value,
2276
+ previousDraftUid: draftUid,
2277
+ draftId
2278
+ });
2279
+ if (data?.draftUid) draftUid = data.draftUid;
2280
+ if (data?.draftId) draftId = data.draftId;
2281
+ if (draftSaveFailed) {
2282
+ draftSaveFailed = false;
2283
+ showDraftStatus("Draft saved", false);
2284
+ } else showDraftStatus(`Draft saved ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`, false);
2285
+ markComposeClean();
2286
+ } catch (e) {
2287
+ console.error("[draft] save failed:", e);
2288
+ draftSaveFailed = true;
2289
+ showDraftStatus(`Draft save failed: ${e?.message || e}`, true);
2290
+ lastDraftContent = "";
2291
+ } finally {
2292
+ draftSaving = false;
2293
+ }
2294
+ }
2295
+ function scheduleDraftSave() {
2296
+ markComposeDirty();
2297
+ if (draftDebounceTimer) clearTimeout(draftDebounceTimer);
2298
+ draftDebounceTimer = setTimeout(() => {
2299
+ draftDebounceTimer = null;
2300
+ requestAnimationFrame(() => {
2301
+ saveDraft2();
2302
+ });
2303
+ }, DRAFT_INPUT_DEBOUNCE_MS);
2304
+ }
2305
+ (async () => {
2306
+ const stored = sessionStorage.getItem("composeInit");
2307
+ if (stored) {
2308
+ sessionStorage.removeItem("composeInit");
2309
+ const init = JSON.parse(stored);
2310
+ if (init.accounts && init.accounts.length > 0) {
2311
+ applyInit(init);
2312
+ getAccounts().then((fresh) => {
2313
+ if (Array.isArray(fresh) && fresh.length > 0) {
2314
+ init.accounts = fresh;
2315
+ try {
2316
+ populateFromOptions(fresh);
2317
+ } catch {
2318
+ }
2319
+ }
2320
+ }).catch(() => {
2321
+ });
2322
+ } else {
2323
+ let fresh = [];
2324
+ try {
2325
+ fresh = await getAccounts();
2326
+ } catch (e) {
2327
+ console.error("Failed to load accounts:", e);
2328
+ }
2329
+ init.accounts = fresh;
2330
+ applyInit(init);
2331
+ }
2332
+ } else {
2333
+ let accounts = [];
2334
+ try {
2335
+ accounts = await getAccounts();
2336
+ } catch (e) {
2337
+ console.error("Failed to load accounts:", e);
2338
+ }
2339
+ populateFromOptions(accounts);
2340
+ toInput.focus();
2341
+ }
2342
+ toInput.addEventListener("input", scheduleDraftSave);
2343
+ ccInput.addEventListener("input", scheduleDraftSave);
2344
+ bccInput.addEventListener("input", scheduleDraftSave);
2345
+ subjectInput.addEventListener("input", scheduleDraftSave);
2346
+ editor.onContentChange(scheduleDraftSave);
2347
+ draftTimer = setInterval(saveDraft2, DRAFT_INTERVAL_MS);
2348
+ window.addEventListener("beforeunload", () => {
2349
+ if (draftDebounceTimer) {
2350
+ clearTimeout(draftDebounceTimer);
2351
+ draftDebounceTimer = null;
2352
+ }
2353
+ saveDraft2();
2354
+ });
2355
+ })();
2356
+ document.addEventListener("keydown", (e) => {
2357
+ if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
2358
+ e.preventDefault();
2359
+ document.getElementById("btn-send")?.click();
2360
+ }
2361
+ });
2362
+ window.addEventListener("blur", () => {
2363
+ try {
2364
+ window.__mailxSaveDraft?.();
2365
+ } catch {
2366
+ }
2367
+ });
2368
+ document.getElementById("btn-send")?.addEventListener("click", async () => {
2369
+ logClientEvent("compose-send-click");
2370
+ const toExpanded = expandGroups(toInput.value);
2371
+ const ccExpanded = expandGroups(ccInput.value);
2372
+ const bccExpanded = expandGroups(bccInput.value);
2373
+ const body = {
2374
+ from: getFromAccountId(),
2375
+ fromAddress: getFromAddress(),
2376
+ to: parseAddrs(toExpanded),
2377
+ cc: parseAddrs(ccExpanded),
2378
+ bcc: parseAddrs(bccExpanded),
2379
+ subject: subjectInput.value,
2380
+ bodyHtml: editor.getHtml(),
2381
+ bodyText: editor.getText(),
2382
+ attachments: attachments.map((a) => ({ filename: a.filename, mimeType: a.mimeType, dataBase64: a.dataBase64 })),
2383
+ // Hand draft identifiers to the daemon so it owns post-send cleanup.
2384
+ // Previously the client fired deleteDraft() as a separate IPC after
2385
+ // send, fire-and-forget with a silent catch — when the IMAP path
2386
+ // hiccuped the draft survived ("draft droppings"). Daemon-side
2387
+ // cleanup queues a sync_action on failure for reliable retry.
2388
+ draftUid: draftUid ?? void 0,
2389
+ draftId: draftId ?? void 0
2390
+ };
2391
+ logClientEvent("compose-send-body-built", { from: body.from, toCount: body.to.length, subjectLen: (body.subject || "").length, bodyHtmlLen: (body.bodyHtml || "").length, atts: body.attachments.length });
2392
+ if (!body.to.length) {
2393
+ logClientEvent("compose-send-rejected-no-to");
2394
+ alert("Please add at least one To recipient.");
2395
+ return;
2396
+ }
2397
+ const emailRe = /^[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+$/;
2398
+ const badAddress = (list) => {
2399
+ if (!list) return null;
2400
+ for (const a of list) {
2401
+ const addr = (a?.address || "").trim();
2402
+ if (!addr) continue;
2403
+ if (!emailRe.test(addr)) return addr;
2404
+ }
2405
+ return null;
2406
+ };
2407
+ const bad = badAddress(body.to) || badAddress(body.cc) || badAddress(body.bcc);
2408
+ if (bad) {
2409
+ logClientEvent("compose-send-rejected-bad-addr", { addr: bad });
2410
+ const statusEl = document.getElementById("compose-status");
2411
+ if (statusEl) statusEl.textContent = `Invalid address: "${bad}"`;
2412
+ else alert(`Invalid address: "${bad}"`);
2413
+ return;
2414
+ }
2415
+ console.log(`[compose] Send clicked: from=${body.from} to=${JSON.stringify(body.to)} subject="${body.subject}" attachments=${body.attachments.length}`);
2416
+ if (draftTimer) {
2417
+ clearInterval(draftTimer);
2418
+ draftTimer = null;
2419
+ }
2420
+ try {
2421
+ const raw = fromInput.value.trim();
2422
+ const known = knownAccounts.some((a) => formatAccountFrom(a) === raw);
2423
+ if (raw && !known && /@.+\./.test(raw)) recordFromHistory(raw);
2424
+ } catch {
2425
+ }
2426
+ const sendStart = Date.now();
2427
+ logClientEvent("compose-send-pre-ipc");
2428
+ try {
2429
+ const reqId = `send-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2430
+ const onMsg = (ev) => {
2431
+ if (!ev.data || ev.data.type !== "mailx-compose-send-result" || ev.data.id !== reqId) return;
2432
+ window.removeEventListener("message", onMsg);
2433
+ if (ev.data.ok) {
2434
+ logClientEvent("compose-send-ipc-resolved", { ms: Date.now() - sendStart });
2435
+ } else {
2436
+ const msg = ev.data.error || "unknown";
2437
+ logClientEvent("compose-send-ipc-rejected", { error: msg, ms: Date.now() - sendStart });
2438
+ try {
2439
+ parent.postMessage({ type: "mailx-send-error", message: msg, accountId: body.from }, "*");
2440
+ } catch {
2441
+ }
2442
+ }
2443
+ };
2444
+ window.addEventListener("message", onMsg);
2445
+ setTimeout(() => window.removeEventListener("message", onMsg), 12e4);
2446
+ parent.postMessage({ type: "mailx-compose-send", id: reqId, body }, "*");
2447
+ logClientEvent("compose-send-ipc-invoked", { via: "parent-relay", reqId });
2448
+ } catch (e) {
2449
+ const msg = e?.message || String(e);
2450
+ logClientEvent("compose-send-ipc-throw", { error: msg });
2451
+ const sendBtn = document.getElementById("btn-send");
2452
+ if (sendBtn) {
2453
+ sendBtn.disabled = false;
2454
+ sendBtn.textContent = "Send";
2455
+ }
2456
+ const statusEl = document.getElementById("compose-status");
2457
+ if (statusEl) statusEl.textContent = `Bridge error: ${msg}`;
2458
+ return;
2459
+ }
2460
+ closeCompose();
2461
+ });
2462
+ function composeHasContent() {
2463
+ return composeHasUserChanges();
2464
+ }
2465
+ function promptSaveOrDiscard() {
2466
+ return new Promise((resolve) => {
2467
+ const overlay = document.createElement("div");
2468
+ overlay.className = "compose-modal-overlay";
2469
+ const box = document.createElement("div");
2470
+ box.className = "compose-modal";
2471
+ const msg = document.createElement("div");
2472
+ msg.className = "compose-modal-msg";
2473
+ msg.textContent = "Save this message as a draft?";
2474
+ const btnRow = document.createElement("div");
2475
+ btnRow.className = "compose-modal-buttons";
2476
+ const mkBtn = (label, choice, primary) => {
2477
+ const b = document.createElement("button");
2478
+ b.type = "button";
2479
+ b.textContent = label;
2480
+ b.className = primary ? "compose-modal-btn primary" : "compose-modal-btn";
2481
+ b.addEventListener("click", () => {
2482
+ cleanup();
2483
+ resolve(choice);
2484
+ });
2485
+ return b;
2486
+ };
2487
+ const cleanup = () => {
2488
+ document.removeEventListener("keydown", onKey);
2489
+ overlay.remove();
2490
+ };
2491
+ const onKey = (e) => {
2492
+ if (e.key === "Escape") {
2493
+ e.preventDefault();
2494
+ cleanup();
2495
+ resolve("cancel");
2496
+ } else if (e.key === "Enter") {
2497
+ e.preventDefault();
2498
+ cleanup();
2499
+ resolve("save");
2500
+ }
2501
+ };
2502
+ document.addEventListener("keydown", onKey);
2503
+ btnRow.appendChild(mkBtn("Save draft", "save", true));
2504
+ btnRow.appendChild(mkBtn("Discard", "discard", false));
2505
+ btnRow.appendChild(mkBtn("Cancel", "cancel", false));
2506
+ box.appendChild(msg);
2507
+ box.appendChild(btnRow);
2508
+ overlay.appendChild(box);
2509
+ document.body.appendChild(overlay);
2510
+ btnRow.firstChild.focus();
2511
+ });
2512
+ }
2513
+ async function handleCloseRequest() {
2514
+ if (!composeHasContent()) {
2515
+ closeCompose();
2516
+ return true;
2517
+ }
2518
+ const choice = await promptSaveOrDiscard();
2519
+ if (choice === "cancel") return false;
2520
+ if (draftDebounceTimer) {
2521
+ clearTimeout(draftDebounceTimer);
2522
+ draftDebounceTimer = null;
2523
+ }
2524
+ if (draftTimer) {
2525
+ clearInterval(draftTimer);
2526
+ draftTimer = null;
2527
+ }
2528
+ if (choice === "save") {
2529
+ try {
2530
+ await saveDraft2();
2531
+ } catch {
2532
+ }
2533
+ } else {
2534
+ if (draftUid || draftId) {
2535
+ try {
2536
+ await deleteDraft(getFromAccountId(), draftUid || 0, draftId || "");
2537
+ } catch {
2538
+ }
2539
+ }
2540
+ }
2541
+ closeCompose();
2542
+ return true;
2543
+ }
2544
+ document.getElementById("btn-discard")?.addEventListener("click", async () => {
2545
+ if (composeHasContent() && !confirm("Discard this draft? Any unsaved content will be lost.")) return;
2546
+ if (draftDebounceTimer) {
2547
+ clearTimeout(draftDebounceTimer);
2548
+ draftDebounceTimer = null;
2549
+ }
2550
+ if (draftTimer) {
2551
+ clearInterval(draftTimer);
2552
+ draftTimer = null;
2553
+ }
2554
+ if (draftUid || draftId) {
2555
+ try {
2556
+ await deleteDraft(getFromAccountId(), draftUid || 0, draftId || "");
2557
+ } catch {
2558
+ }
2559
+ }
2560
+ closeCompose();
2561
+ });
2562
+ var ccRow = document.getElementById("compose-cc-row");
2563
+ var bccRow = document.getElementById("compose-bcc-row");
2564
+ var toggleCcBtn = document.getElementById("btn-toggle-cc");
2565
+ var toggleBccBtn = document.getElementById("btn-toggle-bcc");
2566
+ function setCcVisible(visible) {
2567
+ ccRow.hidden = !visible;
2568
+ toggleCcBtn.classList.toggle("active", visible);
2569
+ if (visible) ccInput.focus();
2570
+ else ccInput.value = "";
2571
+ }
2572
+ function setBccVisible(visible) {
2573
+ bccRow.hidden = !visible;
2574
+ toggleBccBtn.classList.toggle("active", visible);
2575
+ if (visible) bccInput.focus();
2576
+ else bccInput.value = "";
2577
+ }
2578
+ toggleCcBtn?.addEventListener("click", () => setCcVisible(ccRow.hidden));
2579
+ toggleBccBtn?.addEventListener("click", () => setBccVisible(bccRow.hidden));
2580
+ var fileInput = document.getElementById("compose-file");
2581
+ var attEl = document.getElementById("compose-attachments");
2582
+ function renderAttachmentChips() {
2583
+ attEl.innerHTML = "";
2584
+ if (attachments.length === 0) {
2585
+ attEl.hidden = true;
2586
+ return;
2587
+ }
2588
+ attEl.hidden = false;
2589
+ for (let i = 0; i < attachments.length; i++) {
2590
+ const a = attachments[i];
2591
+ const chip = document.createElement("span");
2592
+ chip.className = "compose-att-chip";
2593
+ chip.innerHTML = `\u{1F4CE} ${escapeHtml(a.filename)} (${formatSize(a.size)}) `;
2594
+ const rm = document.createElement("button");
2595
+ rm.type = "button";
2596
+ rm.title = "Remove attachment";
2597
+ rm.textContent = "\u2715";
2598
+ rm.addEventListener("click", () => {
2599
+ attachments.splice(i, 1);
2600
+ renderAttachmentChips();
2601
+ });
2602
+ chip.appendChild(rm);
2603
+ attEl.appendChild(chip);
2604
+ }
2605
+ }
2606
+ function escapeHtml(s) {
2607
+ return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
2608
+ }
2609
+ function formatSize(n) {
2610
+ if (n < 1024) return `${n} B`;
2611
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
2612
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
2613
+ }
2614
+ var attachJustClicked = 0;
2615
+ document.getElementById("btn-attach")?.addEventListener("click", () => {
2616
+ attachJustClicked = Date.now();
2617
+ fileInput?.click();
2618
+ });
2619
+ var wordEditId = null;
2620
+ {
2621
+ const isAndroid = window.mailxapi?.platform === "android" || window.parent?.mailxapi?.platform === "android";
2622
+ if (isAndroid) {
2623
+ const btn = document.getElementById("btn-edit-in-word");
2624
+ if (btn) btn.hidden = true;
2625
+ }
2626
+ }
2627
+ document.getElementById("btn-edit-in-word")?.addEventListener("click", async () => {
2628
+ if (!wordEditId) wordEditId = `compose-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2629
+ showDraftStatus("Opening in Word\u2026", false);
2630
+ try {
2631
+ const result = await openInWord(wordEditId, editor.getHtml());
2632
+ if (!result.ok || result.opener === "none") {
2633
+ showDraftStatus("Couldn't launch an editor. Install Word, LibreOffice, or set a default for .html.", true);
2634
+ return;
2635
+ }
2636
+ const label = result.opener === "word" ? "Word" : result.opener === "libreoffice" ? "LibreOffice" : "your default editor";
2637
+ showDraftStatus(`Editing in ${label} \u2014 saves there will reload here.`, false);
2638
+ showExternalEditHint(label);
2639
+ } catch (e) {
2640
+ showDraftStatus(`Edit-in-Word failed: ${e?.message || e}`, true);
2641
+ }
2642
+ });
2643
+ document.getElementById("btn-compose-help")?.addEventListener("click", async () => {
2644
+ try {
2645
+ const { EDITOR_HELP_MD: EDITOR_HELP_MD2 } = await Promise.resolve().then(() => (init_editor_help(), editor_help_exports));
2646
+ showEditorHelpModal(EDITOR_HELP_MD2);
2647
+ } catch (e) {
2648
+ showDraftStatus(`Couldn't open help: ${e?.message || e}`, true);
2649
+ }
2650
+ });
2651
+ function showEditorHelpModal(md) {
2652
+ if (document.getElementById("mailx-editor-help-modal")) return;
2653
+ const backdrop = document.createElement("div");
2654
+ backdrop.id = "mailx-editor-help-modal";
2655
+ backdrop.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:10001;display:flex;align-items:center;justify-content:center;padding:24px;";
2656
+ const panel = document.createElement("div");
2657
+ panel.style.cssText = "background:var(--color-bg, #fff);color:var(--color-text, #000);border-radius:8px;max-width:780px;max-height:88vh;width:100%;display:flex;flex-direction:column;box-shadow:0 8px 32px rgba(0,0,0,0.4);";
2658
+ const header = document.createElement("div");
2659
+ header.style.cssText = "display:flex;justify-content:space-between;align-items:center;padding:12px 18px;border-bottom:1px solid var(--color-border, #ddd);";
2660
+ header.innerHTML = `<span style="font-weight:600;">Editor help</span><button id="mailx-editor-help-close" style="background:none;border:0;font-size:18px;cursor:pointer;color:var(--color-text);padding:2px 8px;" aria-label="Close">&times;</button>`;
2661
+ const body = document.createElement("div");
2662
+ body.style.cssText = "flex:1;overflow:auto;padding:18px 22px;font:14px/1.55 system-ui;";
2663
+ body.innerHTML = renderMarkdownLite(md);
2664
+ panel.appendChild(header);
2665
+ panel.appendChild(body);
2666
+ backdrop.appendChild(panel);
2667
+ document.body.appendChild(backdrop);
2668
+ const close = () => {
2669
+ backdrop.remove();
2670
+ document.removeEventListener("keydown", onKey, true);
2671
+ };
2672
+ const onKey = (e) => {
2673
+ if (e.key === "Escape") {
2674
+ e.stopPropagation();
2675
+ e.preventDefault();
2676
+ close();
2677
+ }
2678
+ };
2679
+ document.addEventListener("keydown", onKey, true);
2680
+ header.querySelector("#mailx-editor-help-close")?.addEventListener("click", close);
2681
+ backdrop.addEventListener("mousedown", (e) => {
2682
+ if (e.target === backdrop) close();
2683
+ });
2684
+ }
2685
+ function renderMarkdownLite(md) {
2686
+ const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
2687
+ const inline = (s) => esc(s).replace(/`([^`]+)`/g, '<code style="background:var(--color-bg-surface,#f3f3f3);padding:1px 4px;border-radius:3px;">$1</code>').replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/(?<![*\w])\*([^*\n]+)\*(?!\w)/g, "<em>$1</em>").replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
2688
+ const lines = md.split(/\r?\n/);
2689
+ const out = [];
2690
+ let i = 0;
2691
+ while (i < lines.length) {
2692
+ const line = lines[i];
2693
+ const h = /^(#{1,6})\s+(.*)$/.exec(line);
2694
+ if (h) {
2695
+ out.push(`<h${h[1].length} style="margin:18px 0 8px 0;">${inline(h[2])}</h${h[1].length}>`);
2696
+ i++;
2697
+ continue;
2698
+ }
2699
+ if (/^\s*$/.test(line)) {
2700
+ i++;
2701
+ continue;
2702
+ }
2703
+ if (line.includes("|") && /^\s*\|/.test(line)) {
2704
+ const rows = [];
2705
+ while (i < lines.length && /^\s*\|/.test(lines[i])) {
2706
+ rows.push(lines[i].trim().split("|").slice(1, -1).map((s) => s.trim()));
2707
+ i++;
2708
+ }
2709
+ if (rows.length >= 2 && /^[\s\-:]+$/.test(rows[1].join(""))) {
2710
+ const head = rows[0];
2711
+ const data = rows.slice(2);
2712
+ out.push(`<table style="border-collapse:collapse;margin:8px 0;">`);
2713
+ out.push(`<thead><tr>${head.map((h2) => `<th style="border-bottom:2px solid var(--color-border,#ccc);padding:4px 10px;text-align:left;">${inline(h2)}</th>`).join("")}</tr></thead>`);
2714
+ out.push(`<tbody>${data.map((r) => `<tr>${r.map((c) => `<td style="border-bottom:1px solid var(--color-border,#eee);padding:4px 10px;">${inline(c)}</td>`).join("")}</tr>`).join("")}</tbody>`);
2715
+ out.push(`</table>`);
2716
+ continue;
2717
+ }
2718
+ }
2719
+ if (/^\s*[-*]\s+/.test(line)) {
2720
+ const items = [];
2721
+ while (i < lines.length && /^\s*[-*]\s+/.test(lines[i])) {
2722
+ items.push(lines[i].replace(/^\s*[-*]\s+/, ""));
2723
+ i++;
2724
+ }
2725
+ out.push(`<ul style="margin:6px 0 6px 22px;">${items.map((it) => `<li style="margin:3px 0;">${inline(it)}</li>`).join("")}</ul>`);
2726
+ continue;
2727
+ }
2728
+ out.push(`<p style="margin:6px 0;">${inline(line)}</p>`);
2729
+ i++;
2730
+ }
2731
+ return out.join("\n");
2732
+ }
2733
+ function showExternalEditHint(editorLabel) {
2734
+ if (document.getElementById("mailx-extedit-hint")) return;
2735
+ const backdrop = document.createElement("div");
2736
+ backdrop.id = "mailx-extedit-hint";
2737
+ backdrop.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,0.45);z-index:10001;display:flex;align-items:center;justify-content:center;";
2738
+ const panel = document.createElement("div");
2739
+ panel.style.cssText = "background:var(--color-bg, #fff);color:var(--color-text, #000);border:1px solid var(--color-border, #ccc);border-radius:8px;padding:18px 22px;max-width:480px;box-shadow:0 8px 32px rgba(0,0,0,0.4);font:14px/1.5 system-ui;";
2740
+ panel.innerHTML = `
2741
+ <div style="font-weight:600;font-size:16px;margin-bottom:10px;">Editing in ${escapeHtml(editorLabel)}</div>
2742
+ <ol style="margin:0 0 12px 18px;padding:0;">
2743
+ <li>Edit your message in <b>${escapeHtml(editorLabel)}</b>.</li>
2744
+ <li>Save (<b>Ctrl+S</b>). Today, choose <b>"Web Page, Filtered (.htm)"</b> if Word asks for a format \u2014 keep the same filename.</li>
2745
+ <li>Switch back to this window (<b>Alt+Tab</b>). The body will reload here.</li>
2746
+ <li>Click <b>Send</b> in this window when ready.</li>
2747
+ </ol>
2748
+ <div style="text-align:right;margin-top:8px;">
2749
+ <button type="button" id="mailx-extedit-hint-ok" style="padding:6px 16px;border:1px solid var(--color-border, #ccc);background:var(--color-bg-surface, #f6f6f6);border-radius:4px;cursor:pointer;font:inherit;">Got it</button>
2750
+ </div>
2751
+ `;
2752
+ backdrop.appendChild(panel);
2753
+ document.body.appendChild(backdrop);
2754
+ const close = () => {
2755
+ backdrop.remove();
2756
+ document.removeEventListener("keydown", onKey, true);
2757
+ };
2758
+ const onKey = (e) => {
2759
+ if (e.key === "Escape") {
2760
+ e.stopPropagation();
2761
+ e.preventDefault();
2762
+ close();
2763
+ }
2764
+ };
2765
+ document.addEventListener("keydown", onKey, true);
2766
+ panel.querySelector("#mailx-extedit-hint-ok")?.addEventListener("click", close);
2767
+ backdrop.addEventListener("mousedown", (e) => {
2768
+ if (e.target === backdrop) close();
2769
+ });
2770
+ }
2771
+ onEvent((ev) => {
2772
+ if (ev?.type !== "wordEditUpdated") return;
2773
+ if (!wordEditId || ev.editId !== wordEditId) return;
2774
+ try {
2775
+ editor.setHtml(ev.html || "");
2776
+ showDraftStatus("Reloaded edits from external editor.", false);
2777
+ scheduleDraftSave();
2778
+ } catch (e) {
2779
+ showDraftStatus(`Reload failed: ${e?.message || e}`, true);
2780
+ }
2781
+ });
2782
+ window.addEventListener("beforeunload", () => {
2783
+ if (wordEditId) closeWordEdit(wordEditId).catch(() => {
2784
+ });
2785
+ });
2786
+ async function ingestFiles(files) {
2787
+ for (const file of Array.from(files)) {
2788
+ const buf = await file.arrayBuffer();
2789
+ let binary = "";
2790
+ const bytes = new Uint8Array(buf);
2791
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
2792
+ const dataBase64 = btoa(binary);
2793
+ attachments.push({
2794
+ filename: file.name,
2795
+ mimeType: file.type || "application/octet-stream",
2796
+ size: file.size,
2797
+ dataBase64
2798
+ });
2799
+ }
2800
+ renderAttachmentChips();
2801
+ scheduleDraftSave();
2802
+ }
2803
+ fileInput?.addEventListener("change", async () => {
2804
+ if (!fileInput.files) return;
2805
+ await ingestFiles(fileInput.files);
2806
+ fileInput.value = "";
2807
+ });
2808
+ (() => {
2809
+ let dragDepth = 0;
2810
+ const root = document.body;
2811
+ const overlay = document.createElement("div");
2812
+ overlay.id = "compose-drop-overlay";
2813
+ const baseStyle = "position:fixed;inset:0;background:oklch(0.6 0.18 250 / 0.15);border:3px dashed oklch(0.55 0.2 250);z-index:9999;pointer-events:none;align-items:center;justify-content:center;font-size:1.5rem;font-weight:500;color:oklch(0.35 0.2 250)";
2814
+ overlay.style.cssText = baseStyle + ";display:none";
2815
+ overlay.textContent = "Drop files to attach";
2816
+ root.appendChild(overlay);
2817
+ const show = () => {
2818
+ overlay.style.display = "flex";
2819
+ };
2820
+ const hide = () => {
2821
+ overlay.style.display = "none";
2822
+ };
2823
+ const hasFiles = (e) => Array.from(e.dataTransfer?.types || []).includes("Files");
2824
+ root.addEventListener("dragenter", (e) => {
2825
+ if (!hasFiles(e)) return;
2826
+ dragDepth++;
2827
+ show();
2828
+ });
2829
+ root.addEventListener("dragleave", (e) => {
2830
+ if (!hasFiles(e)) return;
2831
+ dragDepth = Math.max(0, dragDepth - 1);
2832
+ if (dragDepth === 0) hide();
2833
+ });
2834
+ root.addEventListener("dragover", (e) => {
2835
+ if (!hasFiles(e)) return;
2836
+ e.preventDefault();
2837
+ if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
2838
+ });
2839
+ root.addEventListener("drop", async (e) => {
2840
+ if (!hasFiles(e)) return;
2841
+ e.preventDefault();
2842
+ dragDepth = 0;
2843
+ hide();
2844
+ const files = e.dataTransfer?.files;
2845
+ if (files && files.length > 0) await ingestFiles(files);
2846
+ });
2847
+ })();
2848
+ window.addEventListener("compose-save-and-close", () => {
2849
+ handleCloseRequest();
2850
+ });
2851
+ window.addEventListener("compose-discard", () => {
2852
+ document.getElementById("btn-discard")?.click();
2853
+ });
2854
+ document.addEventListener("keydown", (e) => {
2855
+ if (e.ctrlKey && e.key === "Enter") {
2856
+ document.getElementById("btn-send")?.click();
2857
+ }
2858
+ if (e.key === "Escape") {
2859
+ if (Date.now() - attachJustClicked < 1500) return;
2860
+ e.preventDefault();
2861
+ handleCloseRequest();
2862
+ }
2863
+ if (e.ctrlKey && (e.key === "k" || e.key === "K")) {
2864
+ const active = document.activeElement;
2865
+ const addressFields = [toInput, ccInput, bccInput];
2866
+ if (addressFields.includes(active)) {
2867
+ e.preventDefault();
2868
+ active.dispatchEvent(new Event("input"));
2869
+ }
2870
+ }
2871
+ });
2872
+ //# sourceMappingURL=compose.bundle.js.map