@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.
- package/README.md +15 -2
- package/bin/mailx.js +56 -1
- package/client/app.js +436 -21
- package/client/components/folder-picker.js +119 -0
- package/client/components/message-list.js +23 -1
- package/client/components/message-viewer.js +140 -22
- package/client/compose/compose.css +50 -0
- package/client/compose/compose.js +68 -12
- package/client/compose/editor.js +51 -7
- package/client/index.html +19 -0
- package/client/lib/api-client.js +12 -0
- package/client/lib/mailxapi.js +16 -7
- package/client/styles/components.css +115 -0
- package/client/styles/layout.css +64 -14
- package/client/styles/variables.css +1 -0
- package/package.json +8 -5
- package/packages/mailx-core/index.d.ts +3 -0
- package/packages/mailx-core/index.js +45 -7
- package/packages/mailx-host/index.d.ts +21 -0
- package/packages/mailx-host/index.js +31 -0
- package/packages/mailx-host/package.json +23 -0
- package/packages/mailx-imap/index.js +33 -0
- package/packages/mailx-service/index.d.ts +18 -1
- package/packages/mailx-service/index.js +198 -6
- package/packages/mailx-service/jsonrpc.js +8 -0
- package/packages/mailx-store/db.js +47 -5
- package/packages/mailx-store-web/android-bootstrap.js +91 -2
- package/packages/mailx-store-web/db.js +4 -1
- package/packages/mailx-store-web/main-thread-host.js +2 -2
- package/packages/mailx-types/index.d.ts +21 -0
- package/tempfix.cmd +77 -0
|
@@ -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;
|
package/client/styles/layout.css
CHANGED
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
|
|
9
9
|
body {
|
|
10
10
|
display: grid;
|
|
11
|
-
|
|
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) —
|
|
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
|
|
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:
|
|
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
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.
|
|
25
|
+
"@bobfrankston/miscinfo": "^1.0.9",
|
|
26
26
|
"@bobfrankston/oauthsupport": "^1.0.24",
|
|
27
|
-
"@bobfrankston/msger": "^0.1.
|
|
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.
|
|
89
|
+
"@bobfrankston/miscinfo": "^1.0.9",
|
|
88
90
|
"@bobfrankston/oauthsupport": "^1.0.24",
|
|
89
|
-
"@bobfrankston/msger": "^0.1.
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|