@bobfrankston/rmfmail 1.2.197 → 1.2.198
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/client/android-bootstrap.bundle.js +72 -15
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js +9 -5
- package/client/app.bundle.js.map +2 -2
- package/client/help/search-help.js +9 -5
- package/client/help/search-help.js.map +1 -1
- package/client/help/search-help.ts +9 -5
- package/client/package.json +1 -1
- package/package.json +5 -5
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +163 -14
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +122 -10
- package/packages/mailx-service/package.json +1 -1
- package/packages/mailx-store-web/package.json +1 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-11396 → node_modules.npmglobalize-stash-64668}/.package-lock.json +0 -0
|
@@ -1234,6 +1234,51 @@ export class MailxService implements MailxApi {
|
|
|
1234
1234
|
console.log(` [word-edit] opened ${filePath} via ${opener}`);
|
|
1235
1235
|
}
|
|
1236
1236
|
|
|
1237
|
+
// ── Word COM autosave sidecar (win32 + Word only) ──
|
|
1238
|
+
// "Save on close should be one step" (Bob 2026-07-30) — better: make
|
|
1239
|
+
// save CONTINUOUS so closing Word is the only step and the save
|
|
1240
|
+
// prompt never appears. A hidden PowerShell loop attaches to the
|
|
1241
|
+
// running Word via COM every ~2 s and calls doc.Save() whenever the
|
|
1242
|
+
// document is dirty. Each save trips the fs.watch above → mammoth →
|
|
1243
|
+
// compose mirrors the edit live. When the document (or Word) closes,
|
|
1244
|
+
// the sidecar exits on its own; stop() kills it if compose closes
|
|
1245
|
+
// first. PowerShell is used as the COM bridge deliberately: Node has
|
|
1246
|
+
// no COM without a native build dep, and allowScripts skips install
|
|
1247
|
+
// scripts so a compiled addon would arrive broken from the registry.
|
|
1248
|
+
let autosaveChild: ReturnType<typeof spawn> | null = null;
|
|
1249
|
+
if (process.platform === "win32" && opener === "word") {
|
|
1250
|
+
try {
|
|
1251
|
+
const psPath = path.join(dir, `${fileBase}-autosave.ps1`);
|
|
1252
|
+
fs.writeFileSync(psPath, [
|
|
1253
|
+
"param([string]$DocPath)",
|
|
1254
|
+
"$deadline = (Get-Date).AddHours(6)",
|
|
1255
|
+
"$attached = $false",
|
|
1256
|
+
"while ((Get-Date) -lt $deadline) {",
|
|
1257
|
+
" Start-Sleep -Seconds 2",
|
|
1258
|
+
" $word = $null",
|
|
1259
|
+
" try { $word = [Runtime.InteropServices.Marshal]::GetActiveObject('Word.Application') }",
|
|
1260
|
+
" catch { if ($attached) { exit 0 } else { continue } }",
|
|
1261
|
+
" $doc = $null",
|
|
1262
|
+
" try { foreach ($d in $word.Documents) { if ($d.FullName -ieq $DocPath) { $doc = $d; break } } } catch { continue }",
|
|
1263
|
+
" if ($null -eq $doc) { if ($attached) { exit 0 } else { continue } }",
|
|
1264
|
+
" $attached = $true",
|
|
1265
|
+
" try { if (-not $doc.Saved) { $doc.Save() } } catch { }",
|
|
1266
|
+
"}",
|
|
1267
|
+
"exit 1",
|
|
1268
|
+
].join("\r\n"), "utf-8");
|
|
1269
|
+
autosaveChild = spawn("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", psPath, filePath],
|
|
1270
|
+
{ detached: false, stdio: "ignore", windowsHide: true });
|
|
1271
|
+
autosaveChild.on("error", () => { autosaveChild = null; });
|
|
1272
|
+
autosaveChild.on("exit", (code) => {
|
|
1273
|
+
console.log(` [word-edit] autosave sidecar exited (${code === 0 ? "document closed" : `code ${code}`})`);
|
|
1274
|
+
try { fs.unlinkSync(psPath); } catch { /* */ }
|
|
1275
|
+
});
|
|
1276
|
+
console.log(` [word-edit] autosave sidecar armed — closing Word is the only step`);
|
|
1277
|
+
} catch (e: any) {
|
|
1278
|
+
console.warn(` [word-edit] autosave sidecar failed to start (${e?.message || e}) — Ctrl+S still works`);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1237
1282
|
// Watch for save events. fs.watch on Windows fires multiple events
|
|
1238
1283
|
// per save (rename + change for atomic replacement); debounce so the
|
|
1239
1284
|
// UI only reloads once per save. Watch the directory rather than the
|
|
@@ -1291,6 +1336,7 @@ export class MailxService implements MailxApi {
|
|
|
1291
1336
|
const stop = () => {
|
|
1292
1337
|
try { watcher.close(); } catch { /* */ }
|
|
1293
1338
|
if (debounce) clearTimeout(debounce);
|
|
1339
|
+
try { autosaveChild?.kill(); } catch { /* */ }
|
|
1294
1340
|
};
|
|
1295
1341
|
this.wordEdits.set(editId, { path: filePath, stop });
|
|
1296
1342
|
|
|
@@ -1742,16 +1788,82 @@ export class MailxService implements MailxApi {
|
|
|
1742
1788
|
// counter. Without this, typing fast left N stale 90-folder IMAP
|
|
1743
1789
|
// sweeps all churning to completion in the background.
|
|
1744
1790
|
const myGen = ++this.serverSearchGen;
|
|
1745
|
-
// Parse qualifiers once; SEARCH runs per folder.
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1791
|
+
// Parse qualifiers once; SEARCH runs per folder. Same query
|
|
1792
|
+
// syntax as local search (Bob 2026-07-30 "IMAP search should be
|
|
1793
|
+
// the same as desktop"): quoted phrases, from:/to:/cc:/subject:,
|
|
1794
|
+
// is:unread etc., after:/before:/date:, uppercase NOT (works
|
|
1795
|
+
// anywhere, including first) and OR — all mapped onto IMAP's
|
|
1796
|
+
// native SEARCH keys (NOT/OR/UNSEEN/SINCE/… are core RFC 3501).
|
|
1797
|
+
// has:attachment has no IMAP key and is ignored server-side.
|
|
1798
|
+
const criteria: any = { not: [], or: [] };
|
|
1799
|
+
const bodyTerms: string[] = [];
|
|
1800
|
+
const unq = (s: string): string => s.replace(/^"|"$/g, "");
|
|
1801
|
+
const parseRelDate = (s: string): Date | null => {
|
|
1802
|
+
const lower = s.toLowerCase().trim();
|
|
1803
|
+
if (lower === "today") { const d = new Date(); d.setHours(0, 0, 0, 0); return d; }
|
|
1804
|
+
if (lower === "yesterday") { const d = new Date(); d.setHours(0, 0, 0, 0); return new Date(d.getTime() - 86400_000); }
|
|
1805
|
+
const rel = lower.match(/^(\d+)([dwmy])$/);
|
|
1806
|
+
if (rel) {
|
|
1807
|
+
const n = parseInt(rel[1]);
|
|
1808
|
+
const ms = rel[2] === "d" ? n * 86400_000 : rel[2] === "w" ? n * 7 * 86400_000
|
|
1809
|
+
: rel[2] === "m" ? n * 30 * 86400_000 : n * 365 * 86400_000;
|
|
1810
|
+
return new Date(Date.now() - ms);
|
|
1811
|
+
}
|
|
1812
|
+
const ts = Date.parse(s);
|
|
1813
|
+
return isNaN(ts) ? null : new Date(ts);
|
|
1814
|
+
};
|
|
1815
|
+
// One token → one criteria fragment (or null for date/unmappable,
|
|
1816
|
+
// which merge into the top level / get dropped).
|
|
1817
|
+
const tokenToFrag = (tok: string): any | null => {
|
|
1818
|
+
const m = tok.match(/^(from|to|cc|subject):(.+)$/i);
|
|
1819
|
+
if (m) return { [m[1].toLowerCase()]: unq(m[2]) };
|
|
1820
|
+
const is = tok.match(/^is:(.+)$/i);
|
|
1821
|
+
if (is) {
|
|
1822
|
+
const v = is[1].toLowerCase();
|
|
1823
|
+
if (v === "unread") return { unseen: true };
|
|
1824
|
+
if (v === "read" || v === "seen") return { seen: true };
|
|
1825
|
+
if (v === "flagged" || v === "starred") return { flagged: true };
|
|
1826
|
+
if (v === "answered") return { answered: true };
|
|
1827
|
+
if (v === "draft") return { draft: true };
|
|
1828
|
+
return null;
|
|
1829
|
+
}
|
|
1830
|
+
if (/^(has|folder|date|after|before):/i.test(tok)) return null;
|
|
1831
|
+
const t = unq(tok);
|
|
1832
|
+
return t ? { body: t } : null;
|
|
1833
|
+
};
|
|
1834
|
+
const tokens = q.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
|
|
1835
|
+
let negate = false;
|
|
1836
|
+
for (let ti = 0; ti < tokens.length; ti++) {
|
|
1837
|
+
const tok = tokens[ti];
|
|
1838
|
+
if (tok === "NOT") { negate = true; continue; }
|
|
1839
|
+
if (tok === "AND" || tok === "OR") { negate = false; continue; }
|
|
1840
|
+
// Dates apply at the top level (negated dates are rare and
|
|
1841
|
+
// ambiguous — treated as positive).
|
|
1842
|
+
const dm = tok.match(/^date:([><]?=?)(.+)$/i) || tok.match(/^(after|before):(.+)$/i);
|
|
1843
|
+
if (dm) {
|
|
1844
|
+
const isAfter = /^after:/i.test(tok) || dm[1] === ">" || dm[1] === ">=";
|
|
1845
|
+
const isBefore = /^before:/i.test(tok) || dm[1] === "<" || dm[1] === "<=";
|
|
1846
|
+
const d = parseRelDate(unq(dm[2]));
|
|
1847
|
+
if (d) {
|
|
1848
|
+
if (isAfter) criteria.since = d;
|
|
1849
|
+
else if (isBefore) criteria.before = d;
|
|
1850
|
+
else { criteria.since = d; criteria.before = new Date(d.getTime() + 86400_000); }
|
|
1851
|
+
}
|
|
1852
|
+
negate = false;
|
|
1853
|
+
continue;
|
|
1854
|
+
}
|
|
1855
|
+
const frag = tokenToFrag(tok);
|
|
1856
|
+
if (!frag) { negate = false; continue; }
|
|
1857
|
+
const orAdjacent = tokens[ti - 1] === "OR" || tokens[ti + 1] === "OR";
|
|
1858
|
+
if (negate) criteria.not.push(frag);
|
|
1859
|
+
else if (orAdjacent) criteria.or.push(frag);
|
|
1860
|
+
else if (frag.body) bodyTerms.push(frag.body);
|
|
1861
|
+
else Object.assign(criteria, frag);
|
|
1862
|
+
negate = false;
|
|
1863
|
+
}
|
|
1864
|
+
if (bodyTerms.length) criteria.body = bodyTerms.length === 1 ? bodyTerms[0] : bodyTerms;
|
|
1865
|
+
if (criteria.not.length === 0) delete criteria.not;
|
|
1866
|
+
if (criteria.or.length === 0) delete criteria.or;
|
|
1755
1867
|
|
|
1756
1868
|
// Server search spans every selectable folder on every enabled
|
|
1757
1869
|
// account — otherwise a message that got moved / was in Sent /
|