@bobfrankston/mailx 1.0.265 → 1.0.283
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/mailx.js +146 -46
- package/client/.msger-window.json +1 -1
- package/client/android.html +1 -0
- package/client/app.js +126 -15
- package/client/components/folder-tree.js +9 -5
- package/client/components/message-list.js +43 -3
- package/client/components/message-viewer.js +47 -14
- package/client/index.html +1 -0
- package/client/lib/api-client.js +5 -2
- package/client/lib/mailxapi.js +5 -2
- package/client/styles/components.css +65 -0
- package/package.json +15 -15
- package/packages/mailx-api/index.js +10 -0
- package/packages/mailx-core/index.d.ts +2 -0
- package/packages/mailx-core/index.js +1 -0
- package/packages/mailx-imap/index.d.ts +22 -1
- package/packages/mailx-imap/index.js +251 -44
- package/packages/mailx-service/index.d.ts +7 -1
- package/packages/mailx-service/index.js +72 -10
- package/packages/mailx-service/jsonrpc.js +3 -1
- package/packages/mailx-settings/cloud.d.ts +11 -2
- package/packages/mailx-settings/cloud.js +66 -39
- package/packages/mailx-settings/index.d.ts +11 -2
- package/packages/mailx-settings/index.js +64 -15
- package/packages/mailx-store/db.d.ts +1 -0
- package/packages/mailx-store/db.js +22 -5
- package/packages/mailx-store-web/web-service.js +13 -2
- package/packages/mailx-types/index.d.ts +6 -0
- package/tsconfig.base.json +1 -0
|
@@ -189,11 +189,21 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
|
|
|
189
189
|
const fromEl = headerEl.querySelector(".mv-from");
|
|
190
190
|
const toEl = headerEl.querySelector(".mv-to");
|
|
191
191
|
fromEl.textContent = formatAddr(msg.from);
|
|
192
|
-
|
|
193
|
-
if (msg.cc?.length)
|
|
194
|
-
|
|
192
|
+
let toLine = `To: ${msg.to.map(formatAddr).join(", ")}`;
|
|
193
|
+
if (msg.cc?.length)
|
|
194
|
+
toLine += ` Cc: ${msg.cc.map(formatAddr).join(", ")}`;
|
|
195
|
+
// Always-visible Delivered-To line — shown when present and not already
|
|
196
|
+
// covered by the To/Cc list. Critical for accounts with multiple aliases
|
|
197
|
+
// where you need to see which one received the message at a glance.
|
|
198
|
+
const toAddrs = (msg.to || []).map((a) => a.address.toLowerCase());
|
|
199
|
+
const ccAddrs = (msg.cc || []).map((a) => a.address.toLowerCase());
|
|
200
|
+
const dt = (msg.deliveredTo || "").toLowerCase();
|
|
201
|
+
if (msg.deliveredTo && !toAddrs.includes(dt) && !ccAddrs.includes(dt)) {
|
|
202
|
+
toLine += ` Delivered-To: ${msg.deliveredTo}`;
|
|
195
203
|
}
|
|
204
|
+
toEl.textContent = toLine;
|
|
196
205
|
headerEl.querySelector(".mv-subject").textContent = msg.subject;
|
|
206
|
+
document.dispatchEvent(new CustomEvent("mailx-message-shown", { detail: { accountId } }));
|
|
197
207
|
// Right-click on email addresses in header: copy name, copy address,
|
|
198
208
|
// copy both, add to contacts, plus reply actions for the whole message.
|
|
199
209
|
for (const el of [fromEl, toEl]) {
|
|
@@ -431,12 +441,12 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
|
|
|
431
441
|
const err = String(msg.bodyError);
|
|
432
442
|
const isTransient = !!msg.bodyErrorTransient;
|
|
433
443
|
const errBanner = document.createElement("div");
|
|
434
|
-
errBanner.className = "mv-error
|
|
435
|
-
errBanner.style.cssText = "margin:1rem;padding:0.75rem 1rem;border:1px solid var(--color-border);border-left:3px solid #d33;background:var(--color-bg-surface);border-radius:4px;font-size:var(--font-size-sm)";
|
|
444
|
+
errBanner.className = "mv-system-message mv-system-error";
|
|
436
445
|
errBanner.innerHTML = `
|
|
437
|
-
<div
|
|
438
|
-
<div
|
|
439
|
-
|
|
446
|
+
<div class="mv-system-tag">mailx</div>
|
|
447
|
+
<div class="mv-system-title">Body unavailable</div>
|
|
448
|
+
<div class="mv-system-body">${err.replace(/[&<>"]/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c] || c))}</div>
|
|
449
|
+
${isTransient ? `<div class="mv-system-actions"><button id="btn-retry-body" class="mv-system-btn">Retry</button></div>` : ""}
|
|
440
450
|
`;
|
|
441
451
|
bodyEl.appendChild(errBanner);
|
|
442
452
|
if (isTransient) {
|
|
@@ -550,20 +560,22 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
|
|
|
550
560
|
// "Message was deleted from the server" — the service already dropped
|
|
551
561
|
// the local row. Remove it from the list so the UI advances to the next
|
|
552
562
|
// message instead of sitting on a stale error banner.
|
|
553
|
-
const
|
|
554
|
-
if (
|
|
563
|
+
const isNotFound = /deleted from the server|isNotFound|not found|Not Found|404/.test(err);
|
|
564
|
+
if (isNotFound) {
|
|
565
|
+
// Drop the stale row so the list auto-advances to the next message
|
|
566
|
+
// (or clears the viewer). Leaves the user a way back on mobile where
|
|
567
|
+
// the viewer takes the whole screen.
|
|
555
568
|
state.removeMessages([{ accountId, uid }]);
|
|
556
569
|
return;
|
|
557
570
|
}
|
|
558
|
-
|
|
559
|
-
if (!isNotFound && retryCount < 3) {
|
|
571
|
+
if (retryCount < 3) {
|
|
560
572
|
retryCount++;
|
|
561
573
|
bodyEl.innerHTML = `<div class="mv-empty">Loading failed: ${err} — retrying (${retryCount}/3)...</div>`;
|
|
562
574
|
setTimeout(() => { if (gen === showMessageGeneration)
|
|
563
575
|
showMessage(accountId, uid, folderId, specialUse, true); }, 3000);
|
|
564
576
|
}
|
|
565
577
|
else {
|
|
566
|
-
bodyEl.innerHTML = `<div class="mv-empty"
|
|
578
|
+
bodyEl.innerHTML = `<div class="mv-empty">Failed to load: ${err}</div>`;
|
|
567
579
|
}
|
|
568
580
|
}
|
|
569
581
|
}
|
|
@@ -658,7 +670,28 @@ ${csp}
|
|
|
658
670
|
}, true);
|
|
659
671
|
document.addEventListener("mouseover", function (e) {
|
|
660
672
|
var a = e.target && e.target.closest ? e.target.closest("a[href]") : null;
|
|
661
|
-
|
|
673
|
+
if (a) {
|
|
674
|
+
var r = a.getBoundingClientRect();
|
|
675
|
+
window.parent.postMessage({ type: "linkHover", url: a.href, rect: { left: r.left, top: r.top, right: r.right, bottom: r.bottom } }, "*");
|
|
676
|
+
} else {
|
|
677
|
+
window.parent.postMessage({ type: "linkHover", url: "" }, "*");
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
// Key forwarding — Delete, Ctrl+D, arrow keys, etc. need to reach app.ts
|
|
681
|
+
// even when focus is inside the sandboxed iframe. Parent-side
|
|
682
|
+
// contentDocument listeners (see installPreviewControls) work on
|
|
683
|
+
// desktop WebView2 but not Android WebView, so we post every keydown
|
|
684
|
+
// that isn't plain typing.
|
|
685
|
+
document.addEventListener("keydown", function (e) {
|
|
686
|
+
var t = e.target;
|
|
687
|
+
if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return;
|
|
688
|
+
// Zoom keys handled by parent-side installPreviewControls; don't double-send.
|
|
689
|
+
if (e.ctrlKey && (e.key === "=" || e.key === "+" || e.key === "-" || e.key === "0")) return;
|
|
690
|
+
window.parent.postMessage({
|
|
691
|
+
type: "previewKey",
|
|
692
|
+
key: e.key, code: e.code,
|
|
693
|
+
ctrlKey: e.ctrlKey, shiftKey: e.shiftKey, altKey: e.altKey, metaKey: e.metaKey,
|
|
694
|
+
}, "*");
|
|
662
695
|
});
|
|
663
696
|
})();
|
|
664
697
|
</script>
|
package/client/index.html
CHANGED
|
@@ -108,6 +108,7 @@
|
|
|
108
108
|
<button class="tb-btn" id="btn-reply-all" title="Reply All (Ctrl+Shift+R)">↩↩</button>
|
|
109
109
|
<button class="tb-btn" id="btn-forward" title="Forward">→</button>
|
|
110
110
|
<button class="tb-btn" id="btn-delete" title="Delete (Del)">🗑</button>
|
|
111
|
+
<button class="tb-btn" id="btn-spam" title="Mark as spam — move to configured spam folder" hidden>⚠</button>
|
|
111
112
|
<button class="tb-btn" id="btn-flag" title="Flag">⚑</button>
|
|
112
113
|
<span style="flex:1"></span>
|
|
113
114
|
<button class="mv-action mv-action-primary" id="mv-edit-draft" hidden>Edit & Send</button>
|
package/client/lib/api-client.js
CHANGED
|
@@ -33,9 +33,9 @@ export function getAccounts() {
|
|
|
33
33
|
export function getFolders(accountId) {
|
|
34
34
|
return ipc().getFolders(accountId);
|
|
35
35
|
}
|
|
36
|
-
export function getMessages(accountId, folderId, page = 1, pageSize = 50) {
|
|
36
|
+
export function getMessages(accountId, folderId, page = 1, pageSize = 50, flaggedOnly = false) {
|
|
37
37
|
abortMessageListRequests();
|
|
38
|
-
return ipc().getMessages(accountId, folderId, page, pageSize);
|
|
38
|
+
return ipc().getMessages(accountId, folderId, page, pageSize, undefined, undefined, undefined, flaggedOnly);
|
|
39
39
|
}
|
|
40
40
|
export function getUnifiedInbox(page = 1, pageSize = 50) {
|
|
41
41
|
abortMessageListRequests();
|
|
@@ -81,6 +81,9 @@ export function moveMessages(accountId, uids, targetFolderId, targetAccountId) {
|
|
|
81
81
|
return moveMessage(accountId, uids[0], targetFolderId, targetAccountId);
|
|
82
82
|
return ipc().moveMessages?.(accountId, uids, targetFolderId, targetAccountId);
|
|
83
83
|
}
|
|
84
|
+
export function markAsSpamMessages(accountId, uids) {
|
|
85
|
+
return ipc().markAsSpamMessages?.(accountId, uids);
|
|
86
|
+
}
|
|
84
87
|
export function undeleteMessage(accountId, uid, folderId) {
|
|
85
88
|
return ipc().undeleteMessage?.(accountId, uid, folderId);
|
|
86
89
|
}
|
package/client/lib/mailxapi.js
CHANGED
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
getFolders: function(accountId) { return callNode("getFolders", { accountId: accountId }); },
|
|
66
66
|
|
|
67
67
|
// Messages
|
|
68
|
-
getMessages: function(accountId, folderId, page, pageSize) {
|
|
69
|
-
return callNode("getMessages", { accountId: accountId, folderId: folderId, page: page, pageSize: pageSize });
|
|
68
|
+
getMessages: function(accountId, folderId, page, pageSize, sort, sortDir, search, flaggedOnly) {
|
|
69
|
+
return callNode("getMessages", { accountId: accountId, folderId: folderId, page: page, pageSize: pageSize, sort: sort, sortDir: sortDir, search: search, flaggedOnly: flaggedOnly });
|
|
70
70
|
},
|
|
71
71
|
getUnifiedInbox: function(page, pageSize) {
|
|
72
72
|
return callNode("getUnifiedInbox", { page: page, pageSize: pageSize });
|
|
@@ -129,6 +129,9 @@
|
|
|
129
129
|
moveMessages: function(accountId, uids, targetFolderId) {
|
|
130
130
|
return callNode("moveMessages", { accountId: accountId, uids: uids, targetFolderId: targetFolderId });
|
|
131
131
|
},
|
|
132
|
+
markAsSpamMessages: function(accountId, uids) {
|
|
133
|
+
return callNode("markAsSpamMessages", { accountId: accountId, uids: uids });
|
|
134
|
+
},
|
|
132
135
|
markFolderRead: function(accountId, folderId) {
|
|
133
136
|
return callNode("markFolderRead", { folderId: folderId });
|
|
134
137
|
},
|
|
@@ -637,6 +637,71 @@ button.tb-menu-item { background: none; border: none; color: inherit; width: 100
|
|
|
637
637
|
font-size: var(--font-size-base);
|
|
638
638
|
}
|
|
639
639
|
|
|
640
|
+
/* ── System-generated message plaque ──
|
|
641
|
+
* Visually distinguished from email content so the user knows the message is
|
|
642
|
+
* from mailx itself (errors, status notices), not part of the rendered email.
|
|
643
|
+
* Inset card with a "mailx" tag chip in the corner. */
|
|
644
|
+
.mv-system-message {
|
|
645
|
+
position: relative;
|
|
646
|
+
margin: var(--gap-lg);
|
|
647
|
+
padding: var(--gap-lg) var(--gap-lg) var(--gap-md);
|
|
648
|
+
background: var(--color-bg-surface);
|
|
649
|
+
border: 1px dashed var(--color-border);
|
|
650
|
+
border-radius: var(--radius-md);
|
|
651
|
+
font-family: var(--font-family-base);
|
|
652
|
+
font-size: var(--font-size-sm);
|
|
653
|
+
color: var(--color-text);
|
|
654
|
+
max-width: 640px;
|
|
655
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
|
|
656
|
+
|
|
657
|
+
.mv-system-tag {
|
|
658
|
+
position: absolute;
|
|
659
|
+
top: -0.6em;
|
|
660
|
+
left: var(--gap-md);
|
|
661
|
+
padding: 0.1em 0.6em;
|
|
662
|
+
background: var(--color-bg);
|
|
663
|
+
border: 1px solid var(--color-border);
|
|
664
|
+
border-radius: var(--radius-sm);
|
|
665
|
+
font-size: 0.7rem;
|
|
666
|
+
font-weight: 600;
|
|
667
|
+
text-transform: uppercase;
|
|
668
|
+
letter-spacing: 0.06em;
|
|
669
|
+
color: var(--color-text-muted);
|
|
670
|
+
}
|
|
671
|
+
.mv-system-title {
|
|
672
|
+
font-weight: 600;
|
|
673
|
+
margin-bottom: var(--gap-xs);
|
|
674
|
+
color: var(--color-text);
|
|
675
|
+
}
|
|
676
|
+
.mv-system-body {
|
|
677
|
+
color: var(--color-text-muted);
|
|
678
|
+
white-space: pre-wrap;
|
|
679
|
+
word-break: break-word;
|
|
680
|
+
line-height: 1.4;
|
|
681
|
+
}
|
|
682
|
+
.mv-system-actions {
|
|
683
|
+
margin-top: var(--gap-sm);
|
|
684
|
+
display: flex;
|
|
685
|
+
gap: var(--gap-sm);
|
|
686
|
+
}
|
|
687
|
+
.mv-system-btn {
|
|
688
|
+
padding: 0.3em 0.9em;
|
|
689
|
+
border: 1px solid var(--color-border);
|
|
690
|
+
border-radius: var(--radius-sm);
|
|
691
|
+
background: var(--color-bg);
|
|
692
|
+
color: var(--color-text);
|
|
693
|
+
cursor: pointer;
|
|
694
|
+
font-size: var(--font-size-sm);
|
|
695
|
+
&:hover { background: var(--color-bg-hover); }
|
|
696
|
+
}
|
|
697
|
+
/* Error variant — red accent on tag, title, and left edge */
|
|
698
|
+
&.mv-system-error {
|
|
699
|
+
border-left: 3px solid oklch(0.55 0.22 25);
|
|
700
|
+
.mv-system-tag { color: oklch(0.55 0.22 25); border-color: oklch(0.55 0.22 25); }
|
|
701
|
+
.mv-system-title { color: oklch(0.55 0.22 25); }
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
640
705
|
/* ── Message Viewer ── */
|
|
641
706
|
|
|
642
707
|
.message-viewer {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.283",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
"postinstall": "node bin/postinstall.js"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@bobfrankston/iflow-direct": "^0.1.
|
|
24
|
-
"@bobfrankston/iflow-node": "^0.1.
|
|
23
|
+
"@bobfrankston/iflow-direct": "^0.1.23",
|
|
24
|
+
"@bobfrankston/iflow-node": "^0.1.7",
|
|
25
25
|
"@bobfrankston/miscinfo": "^1.0.8",
|
|
26
26
|
"@bobfrankston/oauthsupport": "^1.0.24",
|
|
27
|
-
"@bobfrankston/msger": "^0.1.
|
|
27
|
+
"@bobfrankston/msger": "^0.1.320",
|
|
28
28
|
"@capacitor/android": "^8.3.0",
|
|
29
29
|
"@capacitor/cli": "^8.3.0",
|
|
30
30
|
"@capacitor/core": "^8.3.0",
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"quill": "^2.0.3",
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
37
|
"sql.js": "^1.14.1",
|
|
38
|
-
"@bobfrankston/tcp-transport": "^0.1.
|
|
39
|
-
"@bobfrankston/node-tcp-transport": "^0.1.
|
|
40
|
-
"@bobfrankston/smtp-direct": "^0.1.
|
|
41
|
-
"@bobfrankston/mailx-sync": "^0.1.
|
|
38
|
+
"@bobfrankston/tcp-transport": "^0.1.4",
|
|
39
|
+
"@bobfrankston/node-tcp-transport": "^0.1.4",
|
|
40
|
+
"@bobfrankston/smtp-direct": "^0.1.3",
|
|
41
|
+
"@bobfrankston/mailx-sync": "^0.1.6"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/mailparser": "^3.4.6"
|
|
@@ -82,11 +82,11 @@
|
|
|
82
82
|
},
|
|
83
83
|
".transformedSnapshot": {
|
|
84
84
|
"dependencies": {
|
|
85
|
-
"@bobfrankston/iflow-direct": "^0.1.
|
|
86
|
-
"@bobfrankston/iflow-node": "^0.1.
|
|
85
|
+
"@bobfrankston/iflow-direct": "^0.1.23",
|
|
86
|
+
"@bobfrankston/iflow-node": "^0.1.7",
|
|
87
87
|
"@bobfrankston/miscinfo": "^1.0.8",
|
|
88
88
|
"@bobfrankston/oauthsupport": "^1.0.24",
|
|
89
|
-
"@bobfrankston/msger": "^0.1.
|
|
89
|
+
"@bobfrankston/msger": "^0.1.320",
|
|
90
90
|
"@capacitor/android": "^8.3.0",
|
|
91
91
|
"@capacitor/cli": "^8.3.0",
|
|
92
92
|
"@capacitor/core": "^8.3.0",
|
|
@@ -97,10 +97,10 @@
|
|
|
97
97
|
"quill": "^2.0.3",
|
|
98
98
|
"ws": "^8.18.0",
|
|
99
99
|
"sql.js": "^1.14.1",
|
|
100
|
-
"@bobfrankston/tcp-transport": "^0.1.
|
|
101
|
-
"@bobfrankston/node-tcp-transport": "^0.1.
|
|
102
|
-
"@bobfrankston/smtp-direct": "^0.1.
|
|
103
|
-
"@bobfrankston/mailx-sync": "^0.1.
|
|
100
|
+
"@bobfrankston/tcp-transport": "^0.1.4",
|
|
101
|
+
"@bobfrankston/node-tcp-transport": "^0.1.4",
|
|
102
|
+
"@bobfrankston/smtp-direct": "^0.1.3",
|
|
103
|
+
"@bobfrankston/mailx-sync": "^0.1.6"
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
}
|
|
@@ -168,6 +168,16 @@ export function createApiRouter(db, imapManager) {
|
|
|
168
168
|
res.status(500).json({ error: e.message });
|
|
169
169
|
}
|
|
170
170
|
});
|
|
171
|
+
router.post("/messages/spam", async (req, res) => {
|
|
172
|
+
try {
|
|
173
|
+
const { accountId, uids } = req.body;
|
|
174
|
+
const result = await svc.markAsSpamMessages(accountId, uids);
|
|
175
|
+
res.json({ ok: true, ...result });
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
res.status(500).json({ error: e.message });
|
|
179
|
+
}
|
|
180
|
+
});
|
|
171
181
|
// ── Delete (single — kept for backward compat) ──
|
|
172
182
|
router.delete("/message/:accountId/:uid", async (req, res) => {
|
|
173
183
|
try {
|
|
@@ -28,6 +28,7 @@ export declare function getMessages(params: {
|
|
|
28
28
|
sort?: string;
|
|
29
29
|
sortDir?: string;
|
|
30
30
|
search?: string;
|
|
31
|
+
flaggedOnly?: boolean;
|
|
31
32
|
}): import("@bobfrankston/mailx-types").PagedResult<import("@bobfrankston/mailx-types").MessageEnvelope>;
|
|
32
33
|
export declare function getUnifiedInbox(params: {
|
|
33
34
|
page?: number;
|
|
@@ -67,6 +68,7 @@ export declare function getMessage(params: {
|
|
|
67
68
|
hasAttachments: boolean;
|
|
68
69
|
preview: string;
|
|
69
70
|
bodyPath?: string;
|
|
71
|
+
providerId?: string;
|
|
70
72
|
}>;
|
|
71
73
|
export declare function updateFlags(params: {
|
|
72
74
|
accountId: string;
|
|
@@ -19,6 +19,9 @@ export interface ImapManagerEvents {
|
|
|
19
19
|
}>) => void;
|
|
20
20
|
accountError: (accountId: string, error: string, hint: string, isOAuth: boolean) => void;
|
|
21
21
|
configChanged: (filename: string) => void;
|
|
22
|
+
/** Fired after a message body has been written to the local store — lets
|
|
23
|
+
* the UI flip a row's "not-downloaded" indicator without re-rendering. */
|
|
24
|
+
bodyCached: (accountId: string, uid: number) => void;
|
|
22
25
|
}
|
|
23
26
|
export declare class ImapManager extends EventEmitter {
|
|
24
27
|
private configs;
|
|
@@ -55,13 +58,31 @@ export declare class ImapManager extends EventEmitter {
|
|
|
55
58
|
private opsClients;
|
|
56
59
|
/** Operation queues — ensures sequential access per account */
|
|
57
60
|
private opsQueues;
|
|
61
|
+
/** Persistent body-fetch connections — separate from ops so on-demand
|
|
62
|
+
* body reads never queue behind a slow sync operation (bobma's IMAP
|
|
63
|
+
* SEARCH can sit idle for 300s during backfill). */
|
|
64
|
+
private bodyClients;
|
|
65
|
+
/** Per-account backoff after the IMAP server rejected a connection with
|
|
66
|
+
* the per-user+IP cap (Dovecot mail_max_userip_connections). Subsequent
|
|
67
|
+
* body fetches short-circuit until the timestamp passes. */
|
|
68
|
+
private bodyBackoff;
|
|
58
69
|
/** Get (or create) the persistent operational connection for an account.
|
|
59
70
|
* logout() is wrapped as a no-op so legacy callers don't close it. */
|
|
60
71
|
private getOpsClient;
|
|
61
72
|
/** Run an operation on the account's connection — queued, sequential, no concurrency */
|
|
62
73
|
withConnection<T>(accountId: string, fn: (client: any) => Promise<T>): Promise<T>;
|
|
63
|
-
/**
|
|
74
|
+
/** Open IMAP clients per account, used to trace who's opening sockets
|
|
75
|
+
* when we hit the Dovecot per-user+IP connection cap. */
|
|
76
|
+
private openClients;
|
|
77
|
+
/** Create a new IMAP client (internal — callers use getOpsClient or withConnection).
|
|
78
|
+
* `purpose` is a short tag printed alongside the `[conn+]` log so we can tell
|
|
79
|
+
* which code path (sync/idle/body/outbox/move/…) opened each connection. */
|
|
64
80
|
private newClient;
|
|
81
|
+
/** Get (or lazily create) the persistent body-fetch client. Separate from
|
|
82
|
+
* the ops client so body reads never wait on a slow sync command. */
|
|
83
|
+
private getBodyClient;
|
|
84
|
+
/** Drop the body-fetch connection (e.g. after a socket error). */
|
|
85
|
+
private dropBodyClient;
|
|
65
86
|
/** Disconnect the persistent operational connection for an account */
|
|
66
87
|
disconnectOps(accountId: string): Promise<void>;
|
|
67
88
|
/** Legacy API — callers that still create/destroy connections.
|