@bobfrankston/mailx 1.0.162 → 1.0.164
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 +10 -10
- package/package.json +6 -4
- package/packages/mailx-imap/index.d.ts +2 -3
- package/packages/mailx-imap/index.js +6 -8
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-settings/cloud.js +6 -4
- package/tsconfig.base.json +1 -0
- package/again.cmd +0 -3
- package/bin/ager.js +0 -57
- package/blowaway.cmd +0 -3
- package/killmail.cmd +0 -13
- package/launch.psx +0 -65
- package/rebuild.cmd +0 -6
- package/showports.cmd +0 -1
package/bin/mailx.js
CHANGED
|
@@ -231,7 +231,7 @@ if (hasFlag("v") || hasFlag("version")) {
|
|
|
231
231
|
console.log("mailx (version unknown)");
|
|
232
232
|
}
|
|
233
233
|
console.log(` node ${process.version}`);
|
|
234
|
-
console.log(` iflow
|
|
234
|
+
console.log(` iflow-direct ${ver("@bobfrankston/iflow-direct")}`);
|
|
235
235
|
console.log(` miscinfo ${ver("@bobfrankston/miscinfo")}`);
|
|
236
236
|
console.log(` oauth ${ver("@bobfrankston/oauthsupport")}`);
|
|
237
237
|
console.log(` store ${ver("@bobfrankston/mailx-store")}`);
|
|
@@ -500,14 +500,15 @@ async function runTest() {
|
|
|
500
500
|
console.log(`Testing ${account.label || account.id} (${account.email}):`);
|
|
501
501
|
// Test IMAP
|
|
502
502
|
try {
|
|
503
|
-
const {
|
|
503
|
+
const { createAutoImapConfig, CompatImapClient } = await import("@bobfrankston/iflow-direct");
|
|
504
|
+
const { NodeTransport } = await import("@bobfrankston/iflow-node");
|
|
504
505
|
const config = createAutoImapConfig({
|
|
505
506
|
server: account.imap.host,
|
|
506
507
|
port: account.imap.port,
|
|
507
508
|
username: account.imap.user,
|
|
508
509
|
password: account.imap.password
|
|
509
510
|
});
|
|
510
|
-
const client = new
|
|
511
|
+
const client = new CompatImapClient(config, () => new NodeTransport());
|
|
511
512
|
const folders = await client.getFolderList();
|
|
512
513
|
await client.logout();
|
|
513
514
|
console.log(` IMAP: OK (${folders.length} folders)`);
|
|
@@ -524,7 +525,7 @@ async function runTest() {
|
|
|
524
525
|
}
|
|
525
526
|
else if (account.smtp.auth === "oauth2") {
|
|
526
527
|
// Try to get OAuth token
|
|
527
|
-
const { createAutoImapConfig } = await import("@bobfrankston/iflow");
|
|
528
|
+
const { createAutoImapConfig } = await import("@bobfrankston/iflow-direct");
|
|
528
529
|
const config = createAutoImapConfig({
|
|
529
530
|
server: account.imap.host,
|
|
530
531
|
port: account.imap.port,
|
|
@@ -599,11 +600,10 @@ async function main() {
|
|
|
599
600
|
if (setupMode || !hasConfig()) {
|
|
600
601
|
if (!setupMode)
|
|
601
602
|
console.log("No mailx configuration found.");
|
|
602
|
-
//
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|| (
|
|
606
|
-
|| (hasFlag("mail") ? args[args.indexOf("--mail") + 1] || args[args.indexOf("-mail") + 1] : undefined);
|
|
603
|
+
// -email or -mail flag skips the interactive prompt
|
|
604
|
+
const emailFlag = args.findIndex(a => a === "-email" || a === "--email" || a === "-mail" || a === "--mail");
|
|
605
|
+
const emailArg = args.find(a => a.startsWith("-email=") || a.startsWith("--email=") || a.startsWith("-mail=") || a.startsWith("--mail="))?.split("=")[1]
|
|
606
|
+
|| (emailFlag >= 0 ? args[emailFlag + 1] : undefined);
|
|
607
607
|
await runSetup(emailArg);
|
|
608
608
|
}
|
|
609
609
|
// Redirect console to log file — keep terminal clean
|
|
@@ -646,7 +646,7 @@ async function main() {
|
|
|
646
646
|
}
|
|
647
647
|
const db = new MailxDB(getConfigDir());
|
|
648
648
|
const imapManager = new ImapManager(db);
|
|
649
|
-
|
|
649
|
+
// Native client is the only option (iflow-direct)
|
|
650
650
|
const svc = new MailxService(db, imapManager);
|
|
651
651
|
// Open msger in service mode — custom protocol serves files from client dir
|
|
652
652
|
const clientDir = path.join(import.meta.dirname, "..", "client");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.164",
|
|
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,10 +20,11 @@
|
|
|
20
20
|
"postinstall": "node bin/postinstall.js"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@bobfrankston/iflow": "^1.0
|
|
23
|
+
"@bobfrankston/iflow-direct": "^0.1.0",
|
|
24
|
+
"@bobfrankston/iflow-node": "^0.1.0",
|
|
24
25
|
"@bobfrankston/miscinfo": "^1.0.7",
|
|
25
26
|
"@bobfrankston/oauthsupport": "^1.0.20",
|
|
26
|
-
"@bobfrankston/msger": "^0.1.
|
|
27
|
+
"@bobfrankston/msger": "^0.1.214",
|
|
27
28
|
"@capacitor/android": "^8.3.0",
|
|
28
29
|
"@capacitor/cli": "^8.3.0",
|
|
29
30
|
"@capacitor/core": "^8.3.0",
|
|
@@ -54,7 +55,8 @@
|
|
|
54
55
|
"url": "https://github.com/BobFrankston/mailx.git"
|
|
55
56
|
},
|
|
56
57
|
".dependencies": {
|
|
57
|
-
"@bobfrankston/iflow": "file:../MailApps/iflow",
|
|
58
|
+
"@bobfrankston/iflow-direct": "file:../MailApps/iflow-direct",
|
|
59
|
+
"@bobfrankston/iflow-node": "file:../MailApps/iflow-node",
|
|
58
60
|
"@bobfrankston/miscinfo": "file:../../projects/npm/miscinfo",
|
|
59
61
|
"@bobfrankston/oauthsupport": "file:../../projects/oauth/oauthsupport",
|
|
60
62
|
"@bobfrankston/msger": "file:../../utils/msgx/msger",
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* Multi-account IMAP management wrapping iflow.
|
|
4
4
|
* Syncs messages to local store, emits events for new mail.
|
|
5
5
|
*/
|
|
6
|
-
import { ImapClient } from "@bobfrankston/iflow";
|
|
7
6
|
import { MailxDB, FileMessageStore } from "@bobfrankston/mailx-store";
|
|
8
7
|
import type { AccountConfig, MessageEnvelope, Folder } from "@bobfrankston/mailx-types";
|
|
9
8
|
import { EventEmitter } from "node:events";
|
|
@@ -73,11 +72,11 @@ export declare class ImapManager extends EventEmitter {
|
|
|
73
72
|
/** Register an account */
|
|
74
73
|
addAccount(account: AccountConfig): Promise<void>;
|
|
75
74
|
/** Sync folder list for an account */
|
|
76
|
-
syncFolders(accountId: string, client?:
|
|
75
|
+
syncFolders(accountId: string, client?: any): Promise<Folder[]>;
|
|
77
76
|
/** Store a batch of messages to DB immediately — used by onChunk for incremental sync */
|
|
78
77
|
private storeMessages;
|
|
79
78
|
/** Sync messages for a specific folder */
|
|
80
|
-
syncFolder(accountId: string, folderId: number, client?:
|
|
79
|
+
syncFolder(accountId: string, folderId: number, client?: any): Promise<number>;
|
|
81
80
|
/** Sync all folders for all accounts */
|
|
82
81
|
syncAll(): Promise<void>;
|
|
83
82
|
private _syncAll;
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Multi-account IMAP management wrapping iflow.
|
|
4
4
|
* Syncs messages to local store, emits events for new mail.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { createAutoImapConfig, CompatImapClient } from "@bobfrankston/iflow-direct";
|
|
7
|
+
import { NodeTransport } from "@bobfrankston/iflow-node";
|
|
7
8
|
import { FileMessageStore } from "@bobfrankston/mailx-store";
|
|
8
9
|
import { loadSettings, getStorePath, getConfigDir, getHistoryDays } from "@bobfrankston/mailx-settings";
|
|
9
10
|
import { EventEmitter } from "node:events";
|
|
@@ -280,10 +281,7 @@ export class ImapManager extends EventEmitter {
|
|
|
280
281
|
const config = this.configs.get(accountId);
|
|
281
282
|
if (!config)
|
|
282
283
|
throw new Error(`No config for account ${accountId}`);
|
|
283
|
-
|
|
284
|
-
return new CompatImapClient(config, () => new NodeTransport({ rejectUnauthorized: config.rejectUnauthorized !== false }));
|
|
285
|
-
}
|
|
286
|
-
return new ImapClient(config);
|
|
284
|
+
return new CompatImapClient(config, () => new NodeTransport({ rejectUnauthorized: config.rejectUnauthorized !== false }));
|
|
287
285
|
}
|
|
288
286
|
/** Disconnect the persistent operational connection for an account */
|
|
289
287
|
async disconnectOps(accountId) {
|
|
@@ -321,7 +319,7 @@ export class ImapManager extends EventEmitter {
|
|
|
321
319
|
port: account.imap.port,
|
|
322
320
|
username: account.imap.user,
|
|
323
321
|
password: account.imap.password,
|
|
324
|
-
tokenDirectory
|
|
322
|
+
// tokenDirectory is handled by oauthsupport, not iflow-direct
|
|
325
323
|
});
|
|
326
324
|
this.configs.set(account.id, config);
|
|
327
325
|
// Register account in DB
|
|
@@ -447,7 +445,7 @@ export class ImapManager extends EventEmitter {
|
|
|
447
445
|
// Incremental: fetch new messages — metadata only for speed, bodies on demand
|
|
448
446
|
const fetched = await client.fetchMessagesSinceUid(folder.path, highestUid, { source: false });
|
|
449
447
|
// Filter out the last known message (IMAP * always returns at least one)
|
|
450
|
-
messages = fetched.filter(m => m.uid > highestUid);
|
|
448
|
+
messages = fetched.filter((m) => m.uid > highestUid);
|
|
451
449
|
// Gap detection: check for missing UIDs within the range we've already synced
|
|
452
450
|
// Only reconcile between our lowest and highest UID — don't try to fetch the entire folder history
|
|
453
451
|
const existingUids = this.db.getUidsForFolder(accountId, folderId);
|
|
@@ -483,7 +481,7 @@ export class ImapManager extends EventEmitter {
|
|
|
483
481
|
if (oldestDate > 0 && startDate.getTime() < oldestDate) {
|
|
484
482
|
const existingUids = new Set(this.db.getUidsForFolder(accountId, folderId));
|
|
485
483
|
const backfill = await client.fetchMessageByDate(folder.path, startDate, new Date(oldestDate), { source: false });
|
|
486
|
-
const newBackfill = backfill.filter(m => !existingUids.has(m.uid));
|
|
484
|
+
const newBackfill = backfill.filter((m) => !existingUids.has(m.uid));
|
|
487
485
|
if (newBackfill.length > 0) {
|
|
488
486
|
console.log(` ${folder.path}: backfilling ${newBackfill.length} older messages`);
|
|
489
487
|
messages.push(...newBackfill);
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"@bobfrankston/mailx-types": "file:../mailx-types",
|
|
13
13
|
"@bobfrankston/mailx-settings": "file:../mailx-settings",
|
|
14
14
|
"@bobfrankston/mailx-store": "file:../mailx-store",
|
|
15
|
-
"@bobfrankston/iflow": "file:../../../MailApps/iflow",
|
|
15
|
+
"@bobfrankston/iflow-direct": "file:../../../MailApps/iflow-direct",
|
|
16
16
|
"@bobfrankston/oauthsupport": "file:../../../../projects/oauth/oauthsupport",
|
|
17
17
|
"nodemailer": "^7.0.0"
|
|
18
18
|
},
|
|
@@ -30,10 +30,12 @@ function findGoogleCredentials() {
|
|
|
30
30
|
try {
|
|
31
31
|
let dir = import.meta.dirname;
|
|
32
32
|
for (let i = 0; i < 5; i++) {
|
|
33
|
-
for (const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
for (const pkg of ["iflow-direct", "iflow"]) {
|
|
34
|
+
for (const name of ["iflow-credentials.json", "credentials.json"]) {
|
|
35
|
+
const p = path.join(dir, "node_modules", "@bobfrankston", pkg, name);
|
|
36
|
+
if (fs.existsSync(p))
|
|
37
|
+
return p;
|
|
38
|
+
}
|
|
37
39
|
}
|
|
38
40
|
const parent = path.dirname(dir);
|
|
39
41
|
if (parent === dir)
|
package/tsconfig.base.json
CHANGED
package/again.cmd
DELETED
package/bin/ager.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ager — archive .mailx to c:\temp\.mailx## and preserve OAuth tokens
|
|
3
|
-
*
|
|
4
|
-
* Moves ~/.mailx to c:\temp\.mailx01 (or next available number),
|
|
5
|
-
* then copies the tokens directory back so OAuth doesn't need re-consent.
|
|
6
|
-
*
|
|
7
|
-
* Usage: node ager.js
|
|
8
|
-
*/
|
|
9
|
-
import fs from "node:fs";
|
|
10
|
-
import path from "node:path";
|
|
11
|
-
const home = process.env.USERPROFILE || process.env.HOME || ".";
|
|
12
|
-
const source = path.join(home, ".mailx");
|
|
13
|
-
const tempDir = "C:\\temp";
|
|
14
|
-
if (!fs.existsSync(source)) {
|
|
15
|
-
console.error(`No .mailx directory at ${source}`);
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
18
|
-
// Find next available number
|
|
19
|
-
fs.mkdirSync(tempDir, { recursive: true });
|
|
20
|
-
let num = 1;
|
|
21
|
-
while (fs.existsSync(path.join(tempDir, `.mailx${String(num).padStart(2, "0")}`))) {
|
|
22
|
-
num++;
|
|
23
|
-
}
|
|
24
|
-
const dest = path.join(tempDir, `.mailx${String(num).padStart(2, "0")}`);
|
|
25
|
-
// Save tokens before moving
|
|
26
|
-
const tokensDir = path.join(source, "tokens");
|
|
27
|
-
const hasTokens = fs.existsSync(tokensDir);
|
|
28
|
-
const tokenFiles = [];
|
|
29
|
-
if (hasTokens) {
|
|
30
|
-
const walk = (dir, base) => {
|
|
31
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
32
|
-
const full = path.join(dir, entry.name);
|
|
33
|
-
const rel = path.join(base, entry.name);
|
|
34
|
-
if (entry.isDirectory())
|
|
35
|
-
walk(full, rel);
|
|
36
|
-
else
|
|
37
|
-
tokenFiles.push({ rel, data: fs.readFileSync(full) });
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
walk(tokensDir, "");
|
|
41
|
-
console.log(` Saved ${tokenFiles.length} token file(s)`);
|
|
42
|
-
}
|
|
43
|
-
// Move .mailx to temp
|
|
44
|
-
fs.renameSync(source, dest);
|
|
45
|
-
console.log(`Moved ${source} → ${dest}`);
|
|
46
|
-
// Restore tokens to fresh .mailx
|
|
47
|
-
if (tokenFiles.length > 0) {
|
|
48
|
-
const newTokens = path.join(source, "tokens");
|
|
49
|
-
for (const { rel, data } of tokenFiles) {
|
|
50
|
-
const target = path.join(newTokens, rel);
|
|
51
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
52
|
-
fs.writeFileSync(target, data);
|
|
53
|
-
}
|
|
54
|
-
console.log(` Restored tokens to ${newTokens}`);
|
|
55
|
-
}
|
|
56
|
-
console.log("Ready for fresh start.");
|
|
57
|
-
//# sourceMappingURL=ager.js.map
|
package/blowaway.cmd
DELETED
package/killmail.cmd
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
@echo off
|
|
2
|
-
REM Kill mailx server (port 9333), launcher, and clean up stale DB locks
|
|
3
|
-
REM Safe: only kills mailx processes, never all node.exe
|
|
4
|
-
|
|
5
|
-
REM Try graceful exit first, then force kill
|
|
6
|
-
curl -s -m 2 http://localhost:9333/api/exit >nul 2>&1
|
|
7
|
-
timeout /t 1 /nobreak >nul 2>&1
|
|
8
|
-
taskkill /F /IM mailx-app.exe >nul 2>&1 && echo Killed mailx-app.exe || echo mailx-app.exe not running
|
|
9
|
-
killport 9333
|
|
10
|
-
|
|
11
|
-
REM Clean up stale SQLite WAL/SHM files (left by zombie processes)
|
|
12
|
-
if exist "%USERPROFILE%\.mailx\mailx.db-shm" del "%USERPROFILE%\.mailx\mailx.db-shm" && echo Cleaned mailx.db-shm
|
|
13
|
-
if exist "%USERPROFILE%\.mailx\mailx.db-wal" del "%USERPROFILE%\.mailx\mailx.db-wal" && echo Cleaned mailx.db-wal
|
package/launch.psx
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# Launch mailx email client
|
|
2
|
-
#
|
|
3
|
-
# Usage: launch.ps1 [-dev] [-prod] [-restart] [-clear]
|
|
4
|
-
# -dev (default) Start server with --watch for auto-restart on file changes
|
|
5
|
-
# -prod Start server without --watch
|
|
6
|
-
# -restart Kill existing server on port 9333 before starting
|
|
7
|
-
# -clear Clear WebView2 cache before launching (fixes blank window issues)
|
|
8
|
-
#
|
|
9
|
-
# The launcher starts the Node server (if not running) and opens a WebView2 window.
|
|
10
|
-
# Server: http://localhost:9333
|
|
11
|
-
# Log: mailx.log
|
|
12
|
-
# Data: ~/.mailx/ (DB, store, settings pointer)
|
|
13
|
-
|
|
14
|
-
# Clear WebView2 cache if requested
|
|
15
|
-
if ("-clear" -in $args) {
|
|
16
|
-
$wv2dir = "$PSScriptRoot\launcher\bin\mailx-app.exe.WebView2"
|
|
17
|
-
if (Test-Path $wv2dir) {
|
|
18
|
-
Write-Host "Clearing WebView2 cache..."
|
|
19
|
-
Remove-Item -Recurse -Force $wv2dir
|
|
20
|
-
}
|
|
21
|
-
# Remove -clear from args passed to exe
|
|
22
|
-
$args = $args | Where-Object { $_ -ne "-clear" }
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
# Check if mailx-app is already running
|
|
26
|
-
$existing = Get-Process -Name "mailx-app" -ErrorAction SilentlyContinue
|
|
27
|
-
if ($existing -and ("-restart" -notin $args)) {
|
|
28
|
-
Write-Host "mailx is already running (PID $($existing.Id)). Use -restart to force."
|
|
29
|
-
# Try to bring the existing window to front
|
|
30
|
-
Add-Type @"
|
|
31
|
-
using System;
|
|
32
|
-
using System.Runtime.InteropServices;
|
|
33
|
-
public class Win32 {
|
|
34
|
-
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
35
|
-
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
36
|
-
}
|
|
37
|
-
"@
|
|
38
|
-
$hwnd = $existing.MainWindowHandle
|
|
39
|
-
if ($hwnd -ne [IntPtr]::Zero) {
|
|
40
|
-
[Win32]::ShowWindow($hwnd, 9) | Out-Null # SW_RESTORE
|
|
41
|
-
[Win32]::SetForegroundWindow($hwnd) | Out-Null
|
|
42
|
-
}
|
|
43
|
-
exit 0
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
# Copy latest release build to bin/ if newer
|
|
47
|
-
$release = "$PSScriptRoot\launcher\target\release\mailx-app.exe"
|
|
48
|
-
$bin = "$PSScriptRoot\launcher\bin\mailx-app.exe"
|
|
49
|
-
if ((Test-Path $release) -and (Test-Path $bin)) {
|
|
50
|
-
if ((Get-Item $release).LastWriteTime -gt (Get-Item $bin).LastWriteTime) {
|
|
51
|
-
Write-Host "Updating launcher binary..."
|
|
52
|
-
Copy-Item $release $bin -Force -ErrorAction SilentlyContinue
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
# Try pre-built binary first, then release, then debug
|
|
57
|
-
$exe = $bin
|
|
58
|
-
if (-not (Test-Path $exe)) { $exe = $release }
|
|
59
|
-
if (-not (Test-Path $exe)) { $exe = "$PSScriptRoot\launcher\target\debug\mailx-app.exe" }
|
|
60
|
-
if (-not (Test-Path $exe)) {
|
|
61
|
-
Write-Host "Launcher not found. Run: launcher\build.cmd"
|
|
62
|
-
exit 1
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
Start-Process $exe -ArgumentList $args
|
package/rebuild.cmd
DELETED
package/showports.cmd
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
netstat -ano | findstr :9333
|