@bobfrankston/mailx 1.0.209 → 1.0.211
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 +8 -17
- package/client/.msger-window.json +1 -1
- package/client/package.json +3 -0
- package/package.json +3 -3
- package/bin/bin/mailx.js +0 -842
- package/bin/bin/postinstall.js +0 -39
- package/bin/packages/mailx-settings/cloud.js +0 -243
- package/bin/packages/mailx-settings/index.js +0 -643
|
@@ -1,643 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @bobfrankston/mailx-settings
|
|
3
|
-
* Settings management with shared/local split.
|
|
4
|
-
*
|
|
5
|
-
* Files (shared — on OneDrive or similar):
|
|
6
|
-
* accounts.jsonc — IMAP/SMTP account configs
|
|
7
|
-
* preferences.jsonc — UI, sync, font settings
|
|
8
|
-
* allowlist.jsonc — remote content allow-list
|
|
9
|
-
*
|
|
10
|
-
* Local overrides (~/.mailx/):
|
|
11
|
-
* config.jsonc — pointer to shared dir + local-only settings (storePath, historyDays)
|
|
12
|
-
* accounts.jsonc — cached copy, fallback when shared unavailable
|
|
13
|
-
* preferences.jsonc — local overrides merged on top of shared
|
|
14
|
-
* allowlist.jsonc — cached copy
|
|
15
|
-
*
|
|
16
|
-
* The old settings.jsonc is still supported for backward compatibility.
|
|
17
|
-
*/
|
|
18
|
-
import * as fs from "node:fs";
|
|
19
|
-
import * as path from "node:path";
|
|
20
|
-
import { parse as parseJsonc } from "jsonc-parser";
|
|
21
|
-
import { getCloudProvider, gDriveFindOrCreateFolder } from "./cloud.js";
|
|
22
|
-
// ── Paths ──
|
|
23
|
-
const LOCAL_DIR = path.join(process.env.USERPROFILE || process.env.HOME || ".", ".mailx");
|
|
24
|
-
const LOCAL_CONFIG_PATH = path.join(LOCAL_DIR, "config.jsonc");
|
|
25
|
-
const LEGACY_CONFIG_PATH = path.join(LOCAL_DIR, "config.json");
|
|
26
|
-
const DEFAULT_STORE_PATH = path.join(LOCAL_DIR, "mailxstore");
|
|
27
|
-
/** Resolve a path from config — relative to ~/.mailx/, ~ expands to home */
|
|
28
|
-
function resolvePath(p) {
|
|
29
|
-
if (!p)
|
|
30
|
-
return p;
|
|
31
|
-
const home = process.env.USERPROFILE || process.env.HOME || ".";
|
|
32
|
-
// Expand ~ to home directory
|
|
33
|
-
if (p.startsWith("~/") || p.startsWith("~\\"))
|
|
34
|
-
return path.join(home, p.slice(2));
|
|
35
|
-
if (p === "~")
|
|
36
|
-
return home;
|
|
37
|
-
// Absolute path — use as-is
|
|
38
|
-
if (path.isAbsolute(p))
|
|
39
|
-
return p;
|
|
40
|
-
// Relative — resolve from config directory (~/.mailx/)
|
|
41
|
-
return path.resolve(LOCAL_DIR, p);
|
|
42
|
-
}
|
|
43
|
-
function readLocalConfig() {
|
|
44
|
-
// Migrate config.json → config.jsonc
|
|
45
|
-
if (!fs.existsSync(LOCAL_CONFIG_PATH) && fs.existsSync(LEGACY_CONFIG_PATH)) {
|
|
46
|
-
fs.renameSync(LEGACY_CONFIG_PATH, LOCAL_CONFIG_PATH);
|
|
47
|
-
}
|
|
48
|
-
if (!fs.existsSync(LOCAL_CONFIG_PATH))
|
|
49
|
-
return {};
|
|
50
|
-
return readJsonc(LOCAL_CONFIG_PATH) || {};
|
|
51
|
-
}
|
|
52
|
-
/** Resolve provider config to a filesystem path (checks for local Google Drive mount) */
|
|
53
|
-
function resolveProvider(cfg) {
|
|
54
|
-
// Cloud providers use API only — no filesystem mount scanning
|
|
55
|
-
if (cfg.provider === "gdrive" || cfg.provider === "google")
|
|
56
|
-
return undefined;
|
|
57
|
-
if (cfg.provider === "local")
|
|
58
|
-
return resolvePath(cfg.path);
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
/** Pending cloud config for API fallback (set when mount not found) */
|
|
62
|
-
let pendingCloudConfig = null;
|
|
63
|
-
/** Last cloud API error (for UI display) */
|
|
64
|
-
let lastCloudError = null;
|
|
65
|
-
function resolveSharedEntry(entry) {
|
|
66
|
-
if (typeof entry === "string") {
|
|
67
|
-
const p = resolvePath(entry);
|
|
68
|
-
return fs.existsSync(p) ? p : undefined;
|
|
69
|
-
}
|
|
70
|
-
return resolveProvider(entry);
|
|
71
|
-
}
|
|
72
|
-
function getSharedDir() {
|
|
73
|
-
const config = readLocalConfig();
|
|
74
|
-
if (config.sharedDir) {
|
|
75
|
-
const entries = Array.isArray(config.sharedDir) ? config.sharedDir : [config.sharedDir];
|
|
76
|
-
for (const entry of entries) {
|
|
77
|
-
const resolved = resolveSharedEntry(entry);
|
|
78
|
-
if (resolved)
|
|
79
|
-
return resolved;
|
|
80
|
-
}
|
|
81
|
-
// Set pending cloud config for API access
|
|
82
|
-
if (!pendingCloudConfig) {
|
|
83
|
-
const lastProvider = [...entries].reverse().find(e => typeof e !== "string");
|
|
84
|
-
if (lastProvider)
|
|
85
|
-
pendingCloudConfig = lastProvider;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
// Legacy settingsPath no longer used for shared dir — use loadLegacySettings() for reading only.
|
|
89
|
-
return LOCAL_DIR;
|
|
90
|
-
}
|
|
91
|
-
/** Read a file via cloud API (when filesystem mount not available) */
|
|
92
|
-
export async function cloudRead(filename) {
|
|
93
|
-
if (!pendingCloudConfig)
|
|
94
|
-
return null;
|
|
95
|
-
// Ensure we have a folder ID
|
|
96
|
-
if (!pendingCloudConfig.folderId) {
|
|
97
|
-
pendingCloudConfig.folderId = await gDriveFindOrCreateFolder() || undefined;
|
|
98
|
-
if (pendingCloudConfig.folderId)
|
|
99
|
-
saveFolderIdToConfig(pendingCloudConfig.folderId);
|
|
100
|
-
}
|
|
101
|
-
const provider = getCloudProvider(pendingCloudConfig.provider, pendingCloudConfig.folderId);
|
|
102
|
-
if (!provider) {
|
|
103
|
-
lastCloudError = `No cloud provider for ${pendingCloudConfig.provider}`;
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
console.log(` [cloud] Reading ${filename} via ${pendingCloudConfig.provider} API...`);
|
|
107
|
-
const content = await provider.read(filename);
|
|
108
|
-
if (content) {
|
|
109
|
-
lastCloudError = null;
|
|
110
|
-
// Cache locally
|
|
111
|
-
try {
|
|
112
|
-
fs.mkdirSync(LOCAL_DIR, { recursive: true });
|
|
113
|
-
fs.writeFileSync(path.join(LOCAL_DIR, filename), content);
|
|
114
|
-
}
|
|
115
|
-
catch { /* ignore cache write failure */ }
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
lastCloudError = `Could not read ${filename} from ${pendingCloudConfig.provider} (check credentials)`;
|
|
119
|
-
}
|
|
120
|
-
return content;
|
|
121
|
-
}
|
|
122
|
-
/** Write a file via cloud API */
|
|
123
|
-
export async function cloudWrite(filename, content) {
|
|
124
|
-
if (!pendingCloudConfig)
|
|
125
|
-
return false;
|
|
126
|
-
// Ensure we have a folder ID
|
|
127
|
-
if (!pendingCloudConfig.folderId) {
|
|
128
|
-
pendingCloudConfig.folderId = await gDriveFindOrCreateFolder() || undefined;
|
|
129
|
-
if (pendingCloudConfig.folderId)
|
|
130
|
-
saveFolderIdToConfig(pendingCloudConfig.folderId);
|
|
131
|
-
}
|
|
132
|
-
const provider = getCloudProvider(pendingCloudConfig.provider, pendingCloudConfig.folderId);
|
|
133
|
-
if (!provider)
|
|
134
|
-
return false;
|
|
135
|
-
return provider.write(filename, content);
|
|
136
|
-
}
|
|
137
|
-
/** Persist the discovered folder ID back to config.jsonc so we don't search again */
|
|
138
|
-
function saveFolderIdToConfig(folderId) {
|
|
139
|
-
try {
|
|
140
|
-
const config = readLocalConfig();
|
|
141
|
-
if (config.sharedDir && typeof config.sharedDir === "object" && !Array.isArray(config.sharedDir)) {
|
|
142
|
-
config.sharedDir.folderId = folderId;
|
|
143
|
-
atomicWrite(LOCAL_CONFIG_PATH, config);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
catch { /* non-critical */ }
|
|
147
|
-
}
|
|
148
|
-
/** Whether cloud API fallback is active */
|
|
149
|
-
export function isCloudMode() {
|
|
150
|
-
return pendingCloudConfig !== null;
|
|
151
|
-
}
|
|
152
|
-
/** Get storage provider info for display */
|
|
153
|
-
export function getStorageInfo() {
|
|
154
|
-
const config = readLocalConfig();
|
|
155
|
-
if (config.sharedDir) {
|
|
156
|
-
const entries = Array.isArray(config.sharedDir) ? config.sharedDir : [config.sharedDir];
|
|
157
|
-
for (const entry of entries) {
|
|
158
|
-
if (typeof entry === "string") {
|
|
159
|
-
const resolved = resolveSharedEntry(entry);
|
|
160
|
-
if (resolved && resolved !== LOCAL_DIR) {
|
|
161
|
-
return { provider: "local", mode: "local", cloudPath: resolved };
|
|
162
|
-
}
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
// Provider-based entry — check for API mode (folderId) first, then mount
|
|
166
|
-
const name = (entry.provider === "gdrive" || entry.provider === "google") ? "gdrive" : entry.provider;
|
|
167
|
-
if (entry.folderId) {
|
|
168
|
-
// Has folder ID → API mode (don't scan filesystem for mounts)
|
|
169
|
-
return { provider: name, mode: "api", cloudPath: entry.path, cloudError: lastCloudError || undefined };
|
|
170
|
-
}
|
|
171
|
-
const resolved = resolveSharedEntry(entry);
|
|
172
|
-
if (resolved && resolved !== LOCAL_DIR) {
|
|
173
|
-
return { provider: name, mode: "mount", cloudPath: entry.path };
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
// Not mounted and no folderId — check pendingCloudConfig from initCloudConfig()
|
|
177
|
-
if (pendingCloudConfig) {
|
|
178
|
-
const name = (pendingCloudConfig.provider === "gdrive" || pendingCloudConfig.provider === "google") ? "gdrive" : pendingCloudConfig.provider;
|
|
179
|
-
return { provider: name, mode: "api", cloudPath: pendingCloudConfig.path, cloudError: lastCloudError || undefined };
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return { provider: "local", mode: "local" };
|
|
183
|
-
}
|
|
184
|
-
// ── File helpers ──
|
|
185
|
-
/** Read JSON or JSONC file. If exact path not found, tries .json/.jsonc variant. */
|
|
186
|
-
function readJsonc(filePath) {
|
|
187
|
-
let actual = filePath;
|
|
188
|
-
if (!fs.existsSync(actual)) {
|
|
189
|
-
// Try alternate extension
|
|
190
|
-
if (actual.endsWith(".jsonc"))
|
|
191
|
-
actual = actual.replace(/\.jsonc$/, ".json");
|
|
192
|
-
else if (actual.endsWith(".json"))
|
|
193
|
-
actual = actual.replace(/\.json$/, ".jsonc");
|
|
194
|
-
if (!fs.existsSync(actual))
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
try {
|
|
198
|
-
return parseJsonc(fs.readFileSync(actual, "utf-8").replace(/\r/g, ""));
|
|
199
|
-
}
|
|
200
|
-
catch (e) {
|
|
201
|
-
console.error(`Failed to read ${actual}: ${e.message}`);
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
function atomicWrite(filePath, data) {
|
|
206
|
-
const dir = path.dirname(filePath);
|
|
207
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
208
|
-
const tmp = filePath + ".tmp";
|
|
209
|
-
fs.writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
210
|
-
fs.renameSync(tmp, filePath);
|
|
211
|
-
}
|
|
212
|
-
/** Read a config file: shared first, local fallback, merge local overrides */
|
|
213
|
-
function loadFile(filename, defaults) {
|
|
214
|
-
const sharedDir = getSharedDir();
|
|
215
|
-
const sharedPath = path.join(sharedDir, filename);
|
|
216
|
-
const localPath = path.join(LOCAL_DIR, filename);
|
|
217
|
-
// Read shared version
|
|
218
|
-
let data = readJsonc(sharedPath);
|
|
219
|
-
// If no shared, try local
|
|
220
|
-
if (!data)
|
|
221
|
-
data = readJsonc(localPath);
|
|
222
|
-
// If neither exists, use defaults
|
|
223
|
-
if (!data)
|
|
224
|
-
return defaults;
|
|
225
|
-
// Cache shared → local (for offline fallback)
|
|
226
|
-
if (sharedDir !== LOCAL_DIR && fs.existsSync(sharedPath)) {
|
|
227
|
-
try {
|
|
228
|
-
const shared = fs.readFileSync(sharedPath, "utf-8");
|
|
229
|
-
fs.mkdirSync(LOCAL_DIR, { recursive: true });
|
|
230
|
-
fs.writeFileSync(localPath, shared);
|
|
231
|
-
}
|
|
232
|
-
catch { /* ignore cache failures */ }
|
|
233
|
-
}
|
|
234
|
-
return { ...defaults, ...data };
|
|
235
|
-
}
|
|
236
|
-
/** Save a config file to the shared directory (and cloud API if active) */
|
|
237
|
-
function saveFile(filename, data) {
|
|
238
|
-
const sharedDir = getSharedDir();
|
|
239
|
-
atomicWrite(path.join(sharedDir, filename), data);
|
|
240
|
-
// Also update local cache
|
|
241
|
-
if (sharedDir !== LOCAL_DIR) {
|
|
242
|
-
try {
|
|
243
|
-
atomicWrite(path.join(LOCAL_DIR, filename), data);
|
|
244
|
-
}
|
|
245
|
-
catch { /* ignore */ }
|
|
246
|
-
}
|
|
247
|
-
// Write to cloud API if filesystem mount unavailable (creates app-owned file for drive.file scope)
|
|
248
|
-
if (pendingCloudConfig) {
|
|
249
|
-
cloudWrite(filename, JSON.stringify(data, null, 2)).then(ok => {
|
|
250
|
-
if (ok)
|
|
251
|
-
console.log(` [cloud] Saved ${filename} via ${pendingCloudConfig.provider} API`);
|
|
252
|
-
else
|
|
253
|
-
console.error(` [cloud] Failed to save ${filename} via ${pendingCloudConfig.provider} API`);
|
|
254
|
-
}).catch(() => { });
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
const PROVIDERS = {
|
|
258
|
-
"gmail.com": {
|
|
259
|
-
label: "Gmail",
|
|
260
|
-
imap: { host: "imap.gmail.com", port: 993, tls: true, auth: "oauth2" },
|
|
261
|
-
smtp: { host: "smtp.gmail.com", port: 587, tls: true, auth: "oauth2" },
|
|
262
|
-
},
|
|
263
|
-
"googlemail.com": {
|
|
264
|
-
label: "Gmail",
|
|
265
|
-
imap: { host: "imap.gmail.com", port: 993, tls: true, auth: "oauth2" },
|
|
266
|
-
smtp: { host: "smtp.gmail.com", port: 587, tls: true, auth: "oauth2" },
|
|
267
|
-
},
|
|
268
|
-
"outlook.com": {
|
|
269
|
-
label: "Outlook",
|
|
270
|
-
imap: { host: "outlook.office365.com", port: 993, tls: true, auth: "oauth2" },
|
|
271
|
-
smtp: { host: "smtp.office365.com", port: 587, tls: true, auth: "oauth2" },
|
|
272
|
-
},
|
|
273
|
-
"hotmail.com": {
|
|
274
|
-
label: "Hotmail",
|
|
275
|
-
imap: { host: "outlook.office365.com", port: 993, tls: true, auth: "oauth2" },
|
|
276
|
-
smtp: { host: "smtp.office365.com", port: 587, tls: true, auth: "oauth2" },
|
|
277
|
-
},
|
|
278
|
-
"yahoo.com": {
|
|
279
|
-
label: "Yahoo",
|
|
280
|
-
imap: { host: "imap.mail.yahoo.com", port: 993, tls: true, auth: "password" },
|
|
281
|
-
smtp: { host: "smtp.mail.yahoo.com", port: 587, tls: true, auth: "password" },
|
|
282
|
-
},
|
|
283
|
-
"aol.com": {
|
|
284
|
-
label: "AOL",
|
|
285
|
-
imap: { host: "imap.aol.com", port: 993, tls: true, auth: "password" },
|
|
286
|
-
smtp: { host: "smtp.aol.com", port: 587, tls: true, auth: "password" },
|
|
287
|
-
},
|
|
288
|
-
"icloud.com": {
|
|
289
|
-
label: "iCloud",
|
|
290
|
-
imap: { host: "imap.mail.me.com", port: 993, tls: true, auth: "password" },
|
|
291
|
-
smtp: { host: "smtp.mail.me.com", port: 587, tls: true, auth: "password" },
|
|
292
|
-
},
|
|
293
|
-
};
|
|
294
|
-
/** Fill in provider defaults for an account based on email domain */
|
|
295
|
-
function normalizeAccount(acct, globalName) {
|
|
296
|
-
const email = acct.email || "";
|
|
297
|
-
const domain = email.split("@")[1]?.toLowerCase() || "";
|
|
298
|
-
const provider = PROVIDERS[domain];
|
|
299
|
-
const user = acct.imap?.user || acct.user || email;
|
|
300
|
-
return {
|
|
301
|
-
id: acct.id || domain.split(".")[0] || "account",
|
|
302
|
-
name: acct.name || globalName || email.split("@")[0],
|
|
303
|
-
label: acct.label || provider?.label,
|
|
304
|
-
email,
|
|
305
|
-
imap: {
|
|
306
|
-
host: acct.imap?.host || provider?.imap.host || `imap.${domain}`,
|
|
307
|
-
port: acct.imap?.port || provider?.imap.port || 993,
|
|
308
|
-
tls: acct.imap?.tls ?? provider?.imap.tls ?? true,
|
|
309
|
-
auth: acct.imap?.auth || provider?.imap.auth || "password",
|
|
310
|
-
user: acct.imap?.user || user,
|
|
311
|
-
password: acct.imap?.password || acct.password,
|
|
312
|
-
},
|
|
313
|
-
smtp: {
|
|
314
|
-
host: acct.smtp?.host || provider?.smtp.host || `smtp.${domain}`,
|
|
315
|
-
port: acct.smtp?.port || provider?.smtp.port || 587,
|
|
316
|
-
tls: acct.smtp?.tls ?? provider?.smtp.tls ?? true,
|
|
317
|
-
auth: acct.smtp?.auth || provider?.smtp.auth || "password",
|
|
318
|
-
user: acct.smtp?.user || user,
|
|
319
|
-
password: acct.smtp?.password || acct.password,
|
|
320
|
-
},
|
|
321
|
-
enabled: acct.enabled ?? true,
|
|
322
|
-
defaultSend: acct.defaultSend,
|
|
323
|
-
syncContacts: acct.syncContacts ?? (provider?.imap.auth === "oauth2"),
|
|
324
|
-
relayDomains: acct.relayDomains,
|
|
325
|
-
deliveredToPrefix: acct.deliveredToPrefix,
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
// ── Defaults ──
|
|
329
|
-
const DEFAULT_ACCOUNTS = [];
|
|
330
|
-
const DEFAULT_PREFERENCES = {
|
|
331
|
-
ui: {
|
|
332
|
-
theme: "system",
|
|
333
|
-
editor: "quill",
|
|
334
|
-
folderWidth: 220,
|
|
335
|
-
listViewerSplit: 40,
|
|
336
|
-
fontSize: 15,
|
|
337
|
-
},
|
|
338
|
-
sync: {
|
|
339
|
-
intervalMinutes: 5,
|
|
340
|
-
historyDays: 30,
|
|
341
|
-
prefetch: true,
|
|
342
|
-
},
|
|
343
|
-
autocomplete: {
|
|
344
|
-
enabled: false,
|
|
345
|
-
provider: "ollama",
|
|
346
|
-
ollamaUrl: "http://localhost:11434",
|
|
347
|
-
ollamaModel: "qwen2.5-coder:1.5b",
|
|
348
|
-
cloudApiKey: "",
|
|
349
|
-
cloudModel: "claude-sonnet-4-20250514",
|
|
350
|
-
debounceMs: 600,
|
|
351
|
-
maxTokens: 60,
|
|
352
|
-
},
|
|
353
|
-
};
|
|
354
|
-
const DEFAULT_AUTOCOMPLETE = {
|
|
355
|
-
enabled: false,
|
|
356
|
-
provider: "ollama",
|
|
357
|
-
ollamaUrl: "http://localhost:11434",
|
|
358
|
-
ollamaModel: "qwen2.5-coder:1.5b",
|
|
359
|
-
cloudApiKey: "",
|
|
360
|
-
cloudModel: "claude-sonnet-4-20250514",
|
|
361
|
-
debounceMs: 600,
|
|
362
|
-
maxTokens: 60,
|
|
363
|
-
};
|
|
364
|
-
const DEFAULT_ALLOWLIST = {
|
|
365
|
-
senders: [],
|
|
366
|
-
domains: [],
|
|
367
|
-
recipients: [],
|
|
368
|
-
};
|
|
369
|
-
// ── Public API ──
|
|
370
|
-
/** Load account configs */
|
|
371
|
-
export function loadAccounts() {
|
|
372
|
-
const sharedDir = getSharedDir();
|
|
373
|
-
const sharedPath = path.join(sharedDir, "accounts.jsonc");
|
|
374
|
-
const localPath = path.join(LOCAL_DIR, "accounts.jsonc");
|
|
375
|
-
// Try shared first, then local cache
|
|
376
|
-
let accounts = readJsonc(sharedPath);
|
|
377
|
-
if (!accounts)
|
|
378
|
-
accounts = readJsonc(localPath);
|
|
379
|
-
if (accounts?.accounts || Array.isArray(accounts)) {
|
|
380
|
-
// Cache shared to local for offline fallback
|
|
381
|
-
if (sharedDir !== LOCAL_DIR && fs.existsSync(sharedPath)) {
|
|
382
|
-
try {
|
|
383
|
-
fs.mkdirSync(LOCAL_DIR, { recursive: true });
|
|
384
|
-
fs.writeFileSync(localPath, fs.readFileSync(sharedPath, "utf-8"));
|
|
385
|
-
}
|
|
386
|
-
catch { /* ignore */ }
|
|
387
|
-
}
|
|
388
|
-
const raw = accounts.accounts || accounts;
|
|
389
|
-
const globalName = accounts.name || "";
|
|
390
|
-
const result = deduplicateAccounts(raw.map((a) => normalizeAccount(a, globalName)));
|
|
391
|
-
return applyAccountOverrides(result);
|
|
392
|
-
}
|
|
393
|
-
// Legacy: read from settings.jsonc
|
|
394
|
-
const legacy = loadLegacySettings();
|
|
395
|
-
if (legacy?.accounts)
|
|
396
|
-
return applyAccountOverrides(legacy.accounts.map((a) => normalizeAccount(a, legacy.name)));
|
|
397
|
-
return DEFAULT_ACCOUNTS;
|
|
398
|
-
}
|
|
399
|
-
/** Normalize email for dedup — Gmail ignores dots before @ */
|
|
400
|
-
function normalizeEmail(email) {
|
|
401
|
-
const [local, domain] = email.toLowerCase().split("@");
|
|
402
|
-
if (!domain)
|
|
403
|
-
return email.toLowerCase();
|
|
404
|
-
if (domain === "gmail.com" || domain === "googlemail.com") {
|
|
405
|
-
return local.replace(/\./g, "") + "@gmail.com";
|
|
406
|
-
}
|
|
407
|
-
return `${local}@${domain}`;
|
|
408
|
-
}
|
|
409
|
-
/** Remove duplicate accounts (same email after normalization) */
|
|
410
|
-
function deduplicateAccounts(accounts) {
|
|
411
|
-
const seen = new Set();
|
|
412
|
-
return accounts.filter(a => {
|
|
413
|
-
const key = normalizeEmail(a.email);
|
|
414
|
-
if (seen.has(key))
|
|
415
|
-
return false;
|
|
416
|
-
seen.add(key);
|
|
417
|
-
return true;
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
/** Apply local per-account overrides (enabled, etc.) */
|
|
421
|
-
function applyAccountOverrides(accounts) {
|
|
422
|
-
const localConfig = readLocalConfig();
|
|
423
|
-
const overrides = localConfig.accountOverrides;
|
|
424
|
-
if (!overrides)
|
|
425
|
-
return accounts;
|
|
426
|
-
for (const acct of accounts) {
|
|
427
|
-
const ov = overrides[acct.id];
|
|
428
|
-
if (!ov)
|
|
429
|
-
continue;
|
|
430
|
-
if (ov.enabled !== undefined)
|
|
431
|
-
acct.enabled = ov.enabled;
|
|
432
|
-
}
|
|
433
|
-
return accounts;
|
|
434
|
-
}
|
|
435
|
-
/** Load accounts with cloud API fallback (async — use when cloud settings may not be mounted) */
|
|
436
|
-
export async function loadAccountsAsync() {
|
|
437
|
-
// Try sync first (filesystem)
|
|
438
|
-
const accounts = loadAccounts();
|
|
439
|
-
if (accounts.length > 0)
|
|
440
|
-
return accounts;
|
|
441
|
-
// Try cloud API fallback
|
|
442
|
-
if (pendingCloudConfig) {
|
|
443
|
-
console.log(" [cloud] Trying cloud API for accounts...");
|
|
444
|
-
const content = await cloudRead("accounts.jsonc");
|
|
445
|
-
if (content) {
|
|
446
|
-
const data = parseJsonc(content);
|
|
447
|
-
if (data?.accounts || Array.isArray(data)) {
|
|
448
|
-
const raw = data.accounts || data;
|
|
449
|
-
const globalName = data.name || "";
|
|
450
|
-
return applyAccountOverrides(raw.map((a) => normalizeAccount(a, globalName)));
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
// Legacy settings.jsonc is no longer read — use accounts.jsonc only
|
|
454
|
-
}
|
|
455
|
-
return [];
|
|
456
|
-
}
|
|
457
|
-
/** Save account configs */
|
|
458
|
-
/** Save accounts — merges with cloud copy by email (multi-client safe) */
|
|
459
|
-
export async function saveAccounts(accounts) {
|
|
460
|
-
// Merge with cloud: keep all accounts, deduplicate by normalized email
|
|
461
|
-
try {
|
|
462
|
-
const cloudContent = await cloudRead("accounts.jsonc");
|
|
463
|
-
if (cloudContent) {
|
|
464
|
-
const cloud = parseJsonc(cloudContent);
|
|
465
|
-
const cloudAccts = cloud?.accounts || (Array.isArray(cloud) ? cloud : []);
|
|
466
|
-
if (cloudAccts.length > 0) {
|
|
467
|
-
const seen = new Set(accounts.map(a => normalizeEmail(a.email)));
|
|
468
|
-
for (const ca of cloudAccts) {
|
|
469
|
-
if (ca.email && !seen.has(normalizeEmail(ca.email))) {
|
|
470
|
-
accounts.push(ca);
|
|
471
|
-
seen.add(normalizeEmail(ca.email));
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
catch { /* cloud read failed — save local version */ }
|
|
478
|
-
saveFile("accounts.jsonc", { accounts });
|
|
479
|
-
}
|
|
480
|
-
/** Load preferences (shared + local overrides, with legacy fallback) */
|
|
481
|
-
export function loadPreferences() {
|
|
482
|
-
let shared = loadFile("preferences.jsonc", DEFAULT_PREFERENCES);
|
|
483
|
-
// Legacy fallback: read ui/sync from settings.jsonc if preferences.jsonc had only defaults
|
|
484
|
-
const legacy = loadLegacySettings();
|
|
485
|
-
if (legacy?.ui)
|
|
486
|
-
shared = { ...shared, ui: { ...shared.ui, ...legacy.ui } };
|
|
487
|
-
if (legacy?.sync)
|
|
488
|
-
shared = { ...shared, sync: { ...shared.sync, ...legacy.sync } };
|
|
489
|
-
const localConfig = readLocalConfig();
|
|
490
|
-
// Local overrides
|
|
491
|
-
if (localConfig.historyDays !== undefined) {
|
|
492
|
-
shared.sync.historyDays = localConfig.historyDays;
|
|
493
|
-
}
|
|
494
|
-
return {
|
|
495
|
-
ui: { ...DEFAULT_PREFERENCES.ui, ...shared.ui },
|
|
496
|
-
sync: { ...DEFAULT_PREFERENCES.sync, ...shared.sync },
|
|
497
|
-
autocomplete: { ...DEFAULT_AUTOCOMPLETE, ...shared.autocomplete },
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
/** Save preferences */
|
|
501
|
-
export function savePreferences(prefs) {
|
|
502
|
-
saveFile("preferences.jsonc", prefs);
|
|
503
|
-
}
|
|
504
|
-
/** Load autocomplete settings */
|
|
505
|
-
export function loadAutocomplete() {
|
|
506
|
-
const prefs = loadPreferences();
|
|
507
|
-
return prefs.autocomplete;
|
|
508
|
-
}
|
|
509
|
-
/** Save autocomplete settings */
|
|
510
|
-
export function saveAutocomplete(settings) {
|
|
511
|
-
const prefs = loadPreferences();
|
|
512
|
-
prefs.autocomplete = settings;
|
|
513
|
-
savePreferences(prefs);
|
|
514
|
-
}
|
|
515
|
-
/** Load remote content allow-list */
|
|
516
|
-
export function loadAllowlist() {
|
|
517
|
-
return loadFile("allowlist.jsonc", DEFAULT_ALLOWLIST);
|
|
518
|
-
}
|
|
519
|
-
/** Save allow-list — merges with existing cloud copy (multi-client safe) */
|
|
520
|
-
export async function saveAllowlist(list) {
|
|
521
|
-
// Read current cloud version and merge (other clients may have added entries)
|
|
522
|
-
let merged = { ...list };
|
|
523
|
-
try {
|
|
524
|
-
const cloudContent = await cloudRead("allowlist.jsonc");
|
|
525
|
-
if (cloudContent) {
|
|
526
|
-
const cloud = parseJsonc(cloudContent);
|
|
527
|
-
if (cloud) {
|
|
528
|
-
const mergeArrays = (local, remote) => [...new Set([...local, ...remote])];
|
|
529
|
-
merged = {
|
|
530
|
-
senders: mergeArrays(list.senders || [], cloud.senders || []),
|
|
531
|
-
domains: mergeArrays(list.domains || [], cloud.domains || []),
|
|
532
|
-
recipients: mergeArrays(list.recipients || [], cloud.recipients || []),
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
catch { /* cloud read failed — save local version */ }
|
|
538
|
-
saveFile("allowlist.jsonc", merged);
|
|
539
|
-
}
|
|
540
|
-
// ── Legacy compatibility ──
|
|
541
|
-
function loadLegacySettings() {
|
|
542
|
-
const config = readLocalConfig();
|
|
543
|
-
if (config.settingsPath)
|
|
544
|
-
return readJsonc(resolvePath(config.settingsPath));
|
|
545
|
-
// Try shared dir first, then local
|
|
546
|
-
const sharedDir = getSharedDir();
|
|
547
|
-
const shared = readJsonc(path.join(sharedDir, "settings.jsonc"));
|
|
548
|
-
if (shared)
|
|
549
|
-
return shared;
|
|
550
|
-
return readJsonc(path.join(LOCAL_DIR, "settings.jsonc"));
|
|
551
|
-
}
|
|
552
|
-
/** Load settings — unified view combining all files (backward compatible) */
|
|
553
|
-
export function loadSettings() {
|
|
554
|
-
const accounts = loadAccounts();
|
|
555
|
-
const prefs = loadPreferences();
|
|
556
|
-
const localConfig = readLocalConfig();
|
|
557
|
-
return {
|
|
558
|
-
accounts,
|
|
559
|
-
ui: prefs.ui,
|
|
560
|
-
sync: prefs.sync,
|
|
561
|
-
autocomplete: prefs.autocomplete,
|
|
562
|
-
store: {
|
|
563
|
-
basePath: localConfig.storePath || DEFAULT_STORE_PATH,
|
|
564
|
-
compressionBoundaryDays: 365,
|
|
565
|
-
},
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
/** Save settings — writes to split files */
|
|
569
|
-
export async function saveSettings(settings) {
|
|
570
|
-
await saveAccounts(settings.accounts);
|
|
571
|
-
savePreferences({ ui: settings.ui, sync: settings.sync });
|
|
572
|
-
}
|
|
573
|
-
/** Get the local store base path */
|
|
574
|
-
export function getStorePath() {
|
|
575
|
-
const config = readLocalConfig();
|
|
576
|
-
return config.storePath ? resolvePath(config.storePath) : DEFAULT_STORE_PATH;
|
|
577
|
-
}
|
|
578
|
-
/** Get the local data directory (DB, store, etc.) */
|
|
579
|
-
export function getConfigDir() {
|
|
580
|
-
return LOCAL_DIR;
|
|
581
|
-
}
|
|
582
|
-
/** Get the shared settings directory */
|
|
583
|
-
export { getSharedDir };
|
|
584
|
-
// detectSharedDir() removed — cloud storage is configured via API (gdrive/onedrive),
|
|
585
|
-
// not auto-detected from filesystem mounts. Setup form triggers initCloudConfig().
|
|
586
|
-
/** Initialize local config if it doesn't exist */
|
|
587
|
-
export function initLocalConfig(sharedDir, storePath) {
|
|
588
|
-
if (fs.existsSync(LOCAL_CONFIG_PATH) && !sharedDir && !storePath)
|
|
589
|
-
return;
|
|
590
|
-
const existing = readLocalConfig();
|
|
591
|
-
// Use explicit sharedDir or preserve existing — no auto-detection.
|
|
592
|
-
// Cloud storage is configured when user adds an account (initCloudConfig).
|
|
593
|
-
const resolvedSharedDir = sharedDir || existing.sharedDir;
|
|
594
|
-
const config = {
|
|
595
|
-
...existing,
|
|
596
|
-
sharedDir: resolvedSharedDir,
|
|
597
|
-
storePath: storePath || existing.storePath || DEFAULT_STORE_PATH,
|
|
598
|
-
};
|
|
599
|
-
fs.mkdirSync(LOCAL_DIR, { recursive: true });
|
|
600
|
-
atomicWrite(LOCAL_CONFIG_PATH, config);
|
|
601
|
-
}
|
|
602
|
-
/** Initialize config with Google Drive cloud storage.
|
|
603
|
-
* Finds or creates the app-owned "mailx" folder via Drive API and stores its ID.
|
|
604
|
-
* No mount scanning — API only. Existing settings at other paths (e.g., home/.mailx
|
|
605
|
-
* from Desktop sync) must be migrated manually or via config.jsonc importPath. */
|
|
606
|
-
export async function initCloudConfig(provider = "gdrive") {
|
|
607
|
-
const existing = readLocalConfig();
|
|
608
|
-
if (existing.sharedDir)
|
|
609
|
-
return; // Already configured
|
|
610
|
-
// Find or create the "mailx" folder via Drive API
|
|
611
|
-
const folderId = await gDriveFindOrCreateFolder();
|
|
612
|
-
const sharedDir = { provider, path: "mailx", folderId: folderId || undefined };
|
|
613
|
-
const config = { ...existing, sharedDir, storePath: existing.storePath || DEFAULT_STORE_PATH };
|
|
614
|
-
fs.mkdirSync(LOCAL_DIR, { recursive: true });
|
|
615
|
-
atomicWrite(LOCAL_CONFIG_PATH, config);
|
|
616
|
-
pendingCloudConfig = sharedDir;
|
|
617
|
-
console.log(` Initialized cloud config: ${provider} (folder ID: ${folderId || "pending"})`);
|
|
618
|
-
}
|
|
619
|
-
const DEFAULT_SETTINGS = {
|
|
620
|
-
accounts: [],
|
|
621
|
-
ui: DEFAULT_PREFERENCES.ui,
|
|
622
|
-
sync: DEFAULT_PREFERENCES.sync,
|
|
623
|
-
autocomplete: DEFAULT_AUTOCOMPLETE,
|
|
624
|
-
store: { basePath: DEFAULT_STORE_PATH, compressionBoundaryDays: 365 },
|
|
625
|
-
};
|
|
626
|
-
/** Get historyDays for an account: per-account override > system override > shared default */
|
|
627
|
-
export function getHistoryDays(accountId) {
|
|
628
|
-
const localConfig = readLocalConfig();
|
|
629
|
-
if (accountId && localConfig.accountOverrides?.[accountId]?.historyDays !== undefined) {
|
|
630
|
-
return localConfig.accountOverrides[accountId].historyDays;
|
|
631
|
-
}
|
|
632
|
-
if (localConfig.historyDays !== undefined)
|
|
633
|
-
return localConfig.historyDays;
|
|
634
|
-
const prefs = loadPreferences();
|
|
635
|
-
return prefs.sync.historyDays || 0;
|
|
636
|
-
}
|
|
637
|
-
/** Get prefetch setting: download bodies during sync (default true) */
|
|
638
|
-
export function getPrefetch() {
|
|
639
|
-
const prefs = loadPreferences();
|
|
640
|
-
return prefs.sync.prefetch !== false;
|
|
641
|
-
}
|
|
642
|
-
export { DEFAULT_SETTINGS, DEFAULT_ALLOWLIST, DEFAULT_PREFERENCES, DEFAULT_AUTOCOMPLETE, LOCAL_DIR };
|
|
643
|
-
//# sourceMappingURL=index.js.map
|