@bobfrankston/mailx 1.0.306 → 1.0.313

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.
@@ -562,6 +562,88 @@ button.tb-menu-item { background: none; border: none; color: inherit; width: 100
562
562
  outline: 2px solid var(--color-accent);
563
563
  outline-offset: -1px;
564
564
  }
565
+ .mailx-modal-input-error {
566
+ outline: 2px solid oklch(0.65 0.2 25) !important;
567
+ outline-offset: -1px;
568
+ background: color-mix(in oklch, oklch(0.65 0.2 25) 6%, var(--color-bg-surface));
569
+ }
570
+ .mailx-modal-input-error::selection {
571
+ background: oklch(0.65 0.2 25);
572
+ color: #fff;
573
+ }
574
+
575
+ /* Split content area — textarea on the left, help on the right */
576
+ .mailx-modal-split {
577
+ display: grid;
578
+ grid-template-columns: minmax(0, 1fr) minmax(240px, 360px);
579
+ gap: var(--gap-md);
580
+ min-height: 0;
581
+
582
+ &:has(.mailx-help-collapsed) {
583
+ grid-template-columns: minmax(0, 1fr) auto;
584
+ }
585
+ }
586
+ @media (max-width: 900px) {
587
+ .mailx-modal-split { grid-template-columns: 1fr; }
588
+ }
589
+ .mailx-modal-split-left, .mailx-modal-split-right {
590
+ display: flex;
591
+ flex-direction: column;
592
+ min-height: 0;
593
+ }
594
+
595
+ .mailx-help-panel {
596
+ background: var(--color-bg-surface);
597
+ border: 1px solid var(--color-border);
598
+ border-radius: var(--radius-sm);
599
+ padding: var(--gap-sm);
600
+ overflow: auto;
601
+ max-height: 60vh;
602
+ font-size: var(--font-size-sm);
603
+ line-height: 1.4;
604
+
605
+ &.mailx-help-collapsed .mailx-help-body { display: none; }
606
+ &.mailx-help-collapsed { max-height: none; padding: 4px; }
607
+ }
608
+ .mailx-help-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--gap-xs); }
609
+ .mailx-help-toggle {
610
+ background: none;
611
+ border: none;
612
+ color: var(--color-text);
613
+ font-weight: 600;
614
+ cursor: pointer;
615
+ font-size: var(--font-size-sm);
616
+ padding: 2px 4px;
617
+ }
618
+ .mailx-help-toggle:hover { background: var(--color-bg-hover); border-radius: var(--radius-sm); }
619
+
620
+ .mailx-help-body {
621
+ & h1, & h2, & h3 { margin-top: var(--gap-sm); margin-bottom: 4px; font-size: 1em; }
622
+ & h1 { font-size: 1.1em; }
623
+ & p { margin: 4px 0; }
624
+ & ul { margin: 4px 0 4px var(--gap-md); padding: 0; }
625
+ & li { margin: 2px 0; }
626
+ & code {
627
+ font-family: var(--font-mono);
628
+ font-size: 0.9em;
629
+ background: var(--color-bg);
630
+ padding: 1px 4px;
631
+ border-radius: 3px;
632
+ }
633
+ & a { color: var(--color-accent); }
634
+ }
635
+ .mailx-help-code {
636
+ font-family: var(--font-mono);
637
+ font-size: 12px;
638
+ background: var(--color-bg);
639
+ border: 1px solid var(--color-border);
640
+ border-radius: var(--radius-sm);
641
+ padding: var(--gap-xs);
642
+ overflow-x: auto;
643
+ margin: 4px 0;
644
+ white-space: pre;
645
+ }
646
+ .mailx-help-code code { background: none; padding: 0; }
565
647
  .mailx-modal-error {
566
648
  color: oklch(0.65 0.2 25);
567
649
  font-size: var(--font-size-sm);
@@ -593,6 +675,39 @@ button.tb-menu-item { background: none; border: none; color: inherit; width: 100
593
675
  font-weight: 500;
594
676
  }
595
677
  .mailx-modal-btn-primary:hover { filter: brightness(1.1); }
678
+
679
+ /* About dialog */
680
+ .mailx-about { font-size: var(--font-size-sm); line-height: 1.5; }
681
+ .mailx-about-dl {
682
+ display: grid;
683
+ grid-template-columns: auto 1fr;
684
+ gap: 4px var(--gap-md);
685
+ margin: 0;
686
+
687
+ & dt { color: var(--color-text-muted); font-weight: 500; }
688
+ & dd { margin: 0; word-break: break-word; }
689
+ }
690
+ .mailx-about-section {
691
+ font-weight: 600;
692
+ margin-top: var(--gap-sm);
693
+ color: var(--color-text-muted);
694
+ font-size: 0.8rem;
695
+ text-transform: uppercase;
696
+ letter-spacing: 0.05em;
697
+ }
698
+ .mailx-about-accounts ul {
699
+ margin: 4px 0 0 var(--gap-md);
700
+ padding: 0;
701
+ }
702
+ .mailx-about-foot {
703
+ margin-top: var(--gap-sm);
704
+ padding-top: var(--gap-sm);
705
+ border-top: 1px solid var(--color-border);
706
+ color: var(--color-text-muted);
707
+ font-size: 0.8rem;
708
+ text-align: center;
709
+ }
710
+
596
711
  .ml-subject {
597
712
  overflow: hidden;
598
713
  text-overflow: ellipsis;
@@ -8,13 +8,14 @@
8
8
 
9
9
  body {
10
10
  display: grid;
11
- grid-template-columns: var(--folder-width) 1fr;
11
+ /* rail | folders | main */
12
+ grid-template-columns: var(--rail-width, 48px) var(--folder-width) 1fr;
12
13
  grid-template-rows: var(--toolbar-height) auto 1fr var(--statusbar-height);
13
14
  grid-template-areas:
14
- "toolbar toolbar"
15
- "alert alert"
16
- "folders main"
17
- "status status";
15
+ "toolbar toolbar toolbar"
16
+ "alert alert alert"
17
+ "rail folders main"
18
+ "status status status";
18
19
  height: 100vh;
19
20
  overflow: hidden;
20
21
  font-family: var(--font-ui);
@@ -25,11 +26,55 @@ body {
25
26
  }
26
27
 
27
28
  .toolbar { grid-area: toolbar; }
29
+ .icon-rail { grid-area: rail; }
28
30
  .folder-panel { grid-area: folders; display: flex; flex-direction: column; overflow: hidden; }
29
31
  .folder-tree { flex: 1; overflow-y: auto; }
30
32
  .main-area { grid-area: main; }
31
33
  .status-bar { grid-area: status; }
32
34
 
35
+ /* Vertical icon rail (Dovecot/Thunderbird-style). Always visible on
36
+ wide+medium tiers; collapses on narrow (icons move into the hamburger
37
+ menu — TBD; for now hidden on narrow). */
38
+ .icon-rail {
39
+ display: flex;
40
+ flex-direction: column;
41
+ justify-content: space-between;
42
+ background: var(--color-bg-alt, #f4f4f5);
43
+ border-right: 1px solid var(--color-border);
44
+ padding: 6px 0;
45
+ overflow: hidden;
46
+ }
47
+ .rail-top, .rail-bottom {
48
+ display: flex;
49
+ flex-direction: column;
50
+ gap: 2px;
51
+ }
52
+ .rail-btn {
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ width: var(--rail-width, 48px);
57
+ height: 38px;
58
+ border: 0;
59
+ background: transparent;
60
+ cursor: pointer;
61
+ font-size: 16px;
62
+ color: var(--color-text);
63
+ border-left: 3px solid transparent;
64
+ transition: background 0.12s, border-color 0.12s;
65
+ }
66
+ .rail-btn:hover:not([disabled]) {
67
+ background: var(--color-hover, rgba(0,0,0,0.06));
68
+ }
69
+ .rail-btn[data-active="true"] {
70
+ background: var(--color-hover, rgba(0,0,0,0.06));
71
+ border-left-color: var(--color-accent, #1a6dd4);
72
+ }
73
+ .rail-btn[disabled] {
74
+ opacity: 0.35;
75
+ cursor: not-allowed;
76
+ }
77
+
33
78
  /* Main area: message list left, viewer right, vertical splitter */
34
79
  .main-area {
35
80
  display: grid;
@@ -56,22 +101,22 @@ body {
56
101
  background: var(--color-accent);
57
102
  }
58
103
 
59
- /* Responsive: mid-width (tablets, foldables) — hide folders, keep list + preview */
104
+ /* Responsive: mid-width (tablets, foldables) — keep rail + list + viewer; folders overlay */
60
105
  @media (max-width: 1100px) and (min-width: 769px) {
61
106
  body {
62
- grid-template-columns: 1fr;
107
+ grid-template-columns: var(--rail-width, 48px) 1fr;
63
108
  grid-template-rows: var(--toolbar-height) auto 1fr var(--statusbar-height);
64
109
  grid-template-areas:
65
- "toolbar"
66
- "alert"
67
- "main"
68
- "status";
110
+ "toolbar toolbar"
111
+ "alert alert"
112
+ "rail main"
113
+ "status status";
69
114
  }
70
115
 
71
- /* Folder panel: overlay slide-in from left (same as narrow) */
116
+ /* Folder panel: overlay slide-in from left, sitting just to the right of the rail */
72
117
  .folder-panel {
73
118
  position: fixed;
74
- left: -280px;
119
+ left: calc(var(--rail-width, 48px) - 280px);
75
120
  top: var(--toolbar-height);
76
121
  bottom: var(--statusbar-height);
77
122
  width: 280px;
@@ -81,7 +126,7 @@ body {
81
126
  border-right: 1px solid var(--color-border);
82
127
  box-shadow: 2px 0 8px rgba(0,0,0,0.3);
83
128
  }
84
- .folder-panel.open { left: 0; }
129
+ .folder-panel.open { left: var(--rail-width, 48px); }
85
130
 
86
131
  /* Show hamburger */
87
132
  #btn-menu { display: inline-flex !important; }
@@ -122,6 +167,11 @@ body {
122
167
  "status";
123
168
  }
124
169
 
170
+ /* Rail hidden on narrow — its commands fold into the hamburger / toolbar.
171
+ Future work: a slide-in rail behind the hamburger so power-users on phone
172
+ can still reach calendar/contacts/etc. */
173
+ .icon-rail { display: none; }
174
+
125
175
  /* Folder panel: overlay slide-in from left */
126
176
  .folder-panel {
127
177
  position: fixed;
@@ -3,6 +3,7 @@
3
3
 
4
4
  :root {
5
5
  /* Layout */
6
+ --rail-width: 44px; /* vertical icon rail (Thunderbird/Dovecot style) */
6
7
  --folder-width: 220px;
7
8
  --toolbar-height: 44px;
8
9
  --statusbar-height: 24px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.306",
3
+ "version": "1.0.313",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -22,9 +22,10 @@
22
22
  "dependencies": {
23
23
  "@bobfrankston/iflow-direct": "^0.1.23",
24
24
  "@bobfrankston/iflow-node": "^0.1.7",
25
- "@bobfrankston/miscinfo": "^1.0.8",
25
+ "@bobfrankston/miscinfo": "^1.0.9",
26
26
  "@bobfrankston/oauthsupport": "^1.0.24",
27
- "@bobfrankston/msger": "^0.1.327",
27
+ "@bobfrankston/msger": "^0.1.342",
28
+ "@bobfrankston/mailx-host": "^0.1.3",
28
29
  "@capacitor/android": "^8.3.0",
29
30
  "@capacitor/cli": "^8.3.0",
30
31
  "@capacitor/core": "^8.3.0",
@@ -65,6 +66,7 @@
65
66
  "@bobfrankston/miscinfo": "file:../../projects/npm/miscinfo",
66
67
  "@bobfrankston/oauthsupport": "file:../../projects/oauth/oauthsupport",
67
68
  "@bobfrankston/msger": "file:../../utils/msgx/msger",
69
+ "@bobfrankston/mailx-host": "file:packages/mailx-host",
68
70
  "@capacitor/android": "^8.3.0",
69
71
  "@capacitor/cli": "^8.3.0",
70
72
  "@capacitor/core": "^8.3.0",
@@ -84,9 +86,10 @@
84
86
  "dependencies": {
85
87
  "@bobfrankston/iflow-direct": "^0.1.23",
86
88
  "@bobfrankston/iflow-node": "^0.1.7",
87
- "@bobfrankston/miscinfo": "^1.0.8",
89
+ "@bobfrankston/miscinfo": "^1.0.9",
88
90
  "@bobfrankston/oauthsupport": "^1.0.24",
89
- "@bobfrankston/msger": "^0.1.327",
91
+ "@bobfrankston/msger": "^0.1.342",
92
+ "@bobfrankston/mailx-host": "^0.1.3",
90
93
  "@capacitor/android": "^8.3.0",
91
94
  "@capacitor/cli": "^8.3.0",
92
95
  "@capacitor/core": "^8.3.0",
@@ -48,6 +48,9 @@ export declare function getMessage(params: {
48
48
  deliveredTo: string;
49
49
  returnPath: string;
50
50
  listUnsubscribe: string;
51
+ listUnsubscribeMail: string;
52
+ listUnsubscribeHttp: string;
53
+ listUnsubscribeOneClick: boolean;
51
54
  emlPath: string;
52
55
  id: number;
53
56
  accountId: string;
@@ -141,6 +141,9 @@ export async function getMessage(params) {
141
141
  let deliveredTo = "";
142
142
  let returnPath = "";
143
143
  let listUnsubscribe = "";
144
+ let listUnsubscribeMail = "";
145
+ let listUnsubscribeHttp = "";
146
+ let listUnsubscribeOneClick = false;
144
147
  const raw = await imapManager.fetchMessageBody(accountId, envelope.folderId, envelope.uid);
145
148
  if (raw) {
146
149
  const parsed = await simpleParser(raw);
@@ -204,12 +207,9 @@ export async function getMessage(params) {
204
207
  }
205
208
  }
206
209
  returnPath = hdr("return-path").replace(/[<>]/g, "");
207
- // mailparser merges List-* headers into a "list" object
208
- const listHeaders = parsed.headers.get("list");
209
- if (listHeaders?.unsubscribe) {
210
- const unsub = listHeaders.unsubscribe;
211
- listUnsubscribe = unsub.url || (unsub.mail ? `mailto:${unsub.mail}` : "");
212
- }
210
+ ({ listUnsubscribeMail, listUnsubscribeHttp, listUnsubscribeOneClick } =
211
+ parseListUnsubscribe(parsed.headers));
212
+ listUnsubscribe = listUnsubscribeHttp || listUnsubscribeMail;
213
213
  }
214
214
  if (bodyHtml && !allowRemote) {
215
215
  const allowList = loadAllowlist();
@@ -231,7 +231,45 @@ export async function getMessage(params) {
231
231
  // Build .eml file path for "View Source"
232
232
  const storePath = getStorePath();
233
233
  const emlPath = `${storePath}/${accountId}/${envelope.folderId}/${envelope.uid}.eml`;
234
- return { ...envelope, bodyHtml, bodyText, hasRemoteContent, remoteAllowed: allowRemote, attachments, deliveredTo, returnPath, listUnsubscribe, emlPath };
234
+ return { ...envelope, bodyHtml, bodyText, hasRemoteContent, remoteAllowed: allowRemote, attachments, deliveredTo, returnPath, listUnsubscribe, listUnsubscribeMail, listUnsubscribeHttp, listUnsubscribeOneClick, emlPath };
235
+ }
236
+ /** Parse `List-Unsubscribe` (RFC 2369) and `List-Unsubscribe-Post` (RFC 8058).
237
+ * Returns mailto URL, HTTPS URL, and whether one-click POST is allowed.
238
+ * mailparser only exposes ONE of mail/url even when both are present, so we
239
+ * also scan the raw header text for the full set of angle-bracketed URIs. */
240
+ function parseListUnsubscribe(headers) {
241
+ let mail = "";
242
+ let http = "";
243
+ let oneClick = false;
244
+ // Raw header scan — most reliable.
245
+ const raw = headers.get("list-unsubscribe");
246
+ const rawStr = typeof raw === "string" ? raw : (raw && typeof raw.text === "string" ? raw.text : "");
247
+ if (rawStr) {
248
+ const matches = rawStr.match(/<([^>]+)>/g) || [];
249
+ for (const m of matches) {
250
+ const url = m.slice(1, -1).trim();
251
+ if (!mail && /^mailto:/i.test(url))
252
+ mail = url;
253
+ else if (!http && /^https?:/i.test(url))
254
+ http = url;
255
+ }
256
+ }
257
+ // mailparser fallback — for builds where headers.get returns the parsed object.
258
+ if (!mail && !http) {
259
+ const listHeaders = headers.get("list");
260
+ if (listHeaders?.unsubscribe) {
261
+ const unsub = listHeaders.unsubscribe;
262
+ if (unsub.url)
263
+ http = Array.isArray(unsub.url) ? unsub.url[0] : unsub.url;
264
+ if (unsub.mail)
265
+ mail = `mailto:${Array.isArray(unsub.mail) ? unsub.mail[0] : unsub.mail}`;
266
+ }
267
+ }
268
+ const post = headers.get("list-unsubscribe-post");
269
+ const postStr = typeof post === "string" ? post : (post && typeof post.text === "string" ? post.text : "");
270
+ if (postStr && /one-?click/i.test(postStr))
271
+ oneClick = true;
272
+ return { listUnsubscribeMail: mail, listUnsubscribeHttp: http, listUnsubscribeOneClick: oneClick };
235
273
  }
236
274
  export async function updateFlags(params) {
237
275
  const envelope = db.getMessageByUid(params.accountId, params.uid);
@@ -0,0 +1,21 @@
1
+ /**
2
+ * mailx-host — runtime WebView host selector.
3
+ *
4
+ * Today: msger on every platform.
5
+ * Future: lazy-load @bobfrankston/mailx-host-msgview for Mac and niche
6
+ * Linux systems that can't get webkit2gtk-4.1.
7
+ *
8
+ * Callers (bin/mailx.ts) import showMessageBox / showService / setAppName
9
+ * from here instead of @bobfrankston/msger directly, so the swap is a
10
+ * one-line change when msgview arrives.
11
+ */
12
+ import * as msger from "@bobfrankston/msger";
13
+ export type { MessageBoxOptions, MessageBoxResult, ServiceHandle } from "@bobfrankston/msger";
14
+ export type HostName = "msger" | "msgview";
15
+ export declare function selectHost(): HostName;
16
+ export declare const showMessageBox: typeof msger.showMessageBox;
17
+ export declare const showService: typeof msger.showService;
18
+ export declare const setAppName: typeof msger.setAppName;
19
+ export declare const setAppIcon: typeof msger.setAppIcon;
20
+ export declare const hostName: HostName;
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * mailx-host — runtime WebView host selector.
3
+ *
4
+ * Today: msger on every platform.
5
+ * Future: lazy-load @bobfrankston/mailx-host-msgview for Mac and niche
6
+ * Linux systems that can't get webkit2gtk-4.1.
7
+ *
8
+ * Callers (bin/mailx.ts) import showMessageBox / showService / setAppName
9
+ * from here instead of @bobfrankston/msger directly, so the swap is a
10
+ * one-line change when msgview arrives.
11
+ */
12
+ import * as msger from "@bobfrankston/msger";
13
+ export function selectHost() {
14
+ const requested = (process.env.MAILX_HOST || "").toLowerCase();
15
+ if (requested === "msger" || requested === "msgview")
16
+ return requested;
17
+ if (process.platform === "darwin")
18
+ return "msgview";
19
+ return "msger";
20
+ }
21
+ const _hostName = selectHost();
22
+ if (_hostName !== "msger") {
23
+ throw new Error(`mailx-host: "${_hostName}" adapter not implemented yet — set MAILX_HOST=msger to fall back, ` +
24
+ `or implement @bobfrankston/mailx-host-msgview (see docs/host-abstraction-plan.md).`);
25
+ }
26
+ export const showMessageBox = msger.showMessageBox;
27
+ export const showService = msger.showService;
28
+ export const setAppName = msger.setAppName;
29
+ export const setAppIcon = msger.setAppIcon;
30
+ export const hostName = _hostName;
31
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@bobfrankston/mailx-host",
3
+ "version": "0.1.3",
4
+ "description": "Host abstraction for mailx — dispatches to msger or msgview",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "types": "index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "release": "npmglobalize"
11
+ },
12
+ "license": "ISC",
13
+ "dependencies": {
14
+ "@bobfrankston/msger": "file:../../../../utils/msgx/msger"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git@github.com:BobFrankston/mailx-host.git"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ }
23
+ }
@@ -964,6 +964,18 @@ export class ImapManager extends EventEmitter {
964
964
  if (!inboxDone) {
965
965
  console.error(` [sync] ${accountId}: INBOX failed after ${maxAttempts} attempts — will retry next sync cycle`);
966
966
  this.emit("syncError", accountId, `INBOX sync failed after ${maxAttempts} attempts`);
967
+ // Even when sync failed, try to prefetch bodies for messages
968
+ // already in the local DB. Prefetch uses a separate body
969
+ // client (not the ops client that just timed out), so a
970
+ // sync timeout on SELECT/SEARCH doesn't necessarily mean
971
+ // body fetches will also fail. Without this, a server
972
+ // having a slow patch would leave every message with a
973
+ // white "not-downloaded" dot indefinitely until sync
974
+ // recovers — even though prior syncs already populated
975
+ // headers that prefetch can flesh out independently.
976
+ if (getPrefetch()) {
977
+ this.prefetchBodies(accountId).catch(e => console.error(` [prefetch] ${accountId}: ${e.message}`));
978
+ }
967
979
  }
968
980
  }
969
981
  else {
@@ -1431,6 +1443,27 @@ export class ImapManager extends EventEmitter {
1431
1443
  }
1432
1444
  }, 30000);
1433
1445
  this.syncIntervals.set("actions", actionsInterval);
1446
+ // Body prefetch as a first-class background task — independent of
1447
+ // sync success. Prefetch was previously only triggered from inside
1448
+ // sync, so any account with slow/failing IMAP had its "not downloaded"
1449
+ // dots stuck forever even though body fetches use a separate
1450
+ // connection that might succeed. Every 60s, for every account, fire
1451
+ // prefetchBodies() (cheap when body_path is already populated — just a
1452
+ // DB query that returns 0 rows; the prefetchingAccounts guard
1453
+ // short-circuits concurrent triggers).
1454
+ if (getPrefetch()) {
1455
+ const kickPrefetch = () => {
1456
+ for (const [accountId] of this.configs) {
1457
+ this.prefetchBodies(accountId).catch(e => console.error(` [prefetch] ${accountId}: ${e?.message || e}`));
1458
+ }
1459
+ };
1460
+ // Fire once now so the "not downloaded" dots start filling in
1461
+ // immediately on app start, don't make the user wait a minute.
1462
+ setTimeout(kickPrefetch, 2000);
1463
+ const prefetchInterval = setInterval(kickPrefetch, 60000);
1464
+ this.syncIntervals.set("prefetch", prefetchInterval);
1465
+ console.log(` [periodic] body prefetch every 60s (independent of sync)`);
1466
+ }
1434
1467
  // Full sync (all folders + IDLE restart) at configured interval
1435
1468
  const fullInterval = setInterval(async () => {
1436
1469
  console.log(` [periodic] Full sync at ${new Date().toLocaleTimeString()}`);
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { MailxDB } from "@bobfrankston/mailx-store";
7
7
  import { ImapManager } from "@bobfrankston/mailx-imap";
8
- import type { Folder, AutocompleteRequest, AutocompleteResponse, AutocompleteSettings } from "@bobfrankston/mailx-types";
8
+ import type { Folder, AutocompleteRequest, AutocompleteResponse, AutocompleteSettings, AiTransformRequest, AiTransformResponse } from "@bobfrankston/mailx-types";
9
9
  export declare class MailxService {
10
10
  private db;
11
11
  private imapManager;
@@ -15,6 +15,15 @@ export declare class MailxService {
15
15
  getUnifiedInbox(page?: number, pageSize?: number): any;
16
16
  getMessages(accountId: string, folderId: number, page?: number, pageSize?: number, sort?: string, sortDir?: string, search?: string, flaggedOnly?: boolean): any;
17
17
  getMessage(accountId: string, uid: number, allowRemote?: boolean, folderId?: number): Promise<any>;
18
+ /** RFC 8058 one-click unsubscribe: POST `List-Unsubscribe=One-Click` to the
19
+ * HTTPS URL the message's List-Unsubscribe header advertised. Done server-
20
+ * side because the unsubscribe endpoint usually doesn't set CORS headers,
21
+ * so a browser-side fetch would be blocked. */
22
+ unsubscribeOneClick(url: string): Promise<{
23
+ ok: boolean;
24
+ status: number;
25
+ statusText: string;
26
+ }>;
18
27
  updateFlags(accountId: string, uid: number, flags: string[]): Promise<void>;
19
28
  allowRemoteContent(type: "sender" | "domain" | "recipient", value: string): Promise<void>;
20
29
  search(q: string, page?: number, pageSize?: number, scope?: string, accountId?: string, folderId?: number): Promise<any>;
@@ -67,6 +76,9 @@ export declare class MailxService {
67
76
  * Names are whitelisted so the UI can't read arbitrary files.
68
77
  * `config.jsonc` is the local per-machine config (not cloud-synced). */
69
78
  readJsoncFile(name: string): Promise<string | null>;
79
+ /** Return the help section for a named config file, extracted from docs/config-help.md.
80
+ * Matches a level-2 heading whose text equals the filename. Returns markdown. */
81
+ readConfigHelp(name: string): Promise<string>;
70
82
  /** Write a JSONC config file. Validates that the content parses as JSONC
71
83
  * (loosely — strips comments/trailing commas) before writing. */
72
84
  writeJsoncFile(name: string, content: string): Promise<void>;
@@ -89,5 +101,10 @@ export declare class MailxService {
89
101
  getAutocompleteSettings(): AutocompleteSettings;
90
102
  saveAutocompleteSettings(settings: AutocompleteSettings): void;
91
103
  autocomplete(req: AutocompleteRequest): Promise<AutocompleteResponse>;
104
+ /** Generic AI text transform — translate / proofread / summarize.
105
+ * Shares the autocomplete provider config (provider, key, model). Each
106
+ * feature has its own opt-in toggle (translateEnabled / proofreadEnabled),
107
+ * default false. Returns empty text + reason when disabled or on error. */
108
+ aiTransform(req: AiTransformRequest): Promise<AiTransformResponse>;
92
109
  }
93
110
  //# sourceMappingURL=index.d.ts.map