@bobfrankston/rmfmail 1.2.197 → 1.2.199
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/compose/compose.bundle.js +49 -1
- package/client/compose/compose.bundle.js.map +3 -3
- package/client/compose/editor.js +34 -0
- package/client/compose/editor.js.map +1 -1
- package/client/compose/editor.ts +29 -0
- package/client/compose/paste-markdown.js +0 -0
- package/client/compose/paste-markdown.js.map +1 -1
- package/client/compose/paste-markdown.ts +0 -0
- 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 +9 -9
- 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-58008}/.package-lock.json +0 -0
|
@@ -1231,6 +1231,53 @@ export class MailxService {
|
|
|
1231
1231
|
else {
|
|
1232
1232
|
console.log(` [word-edit] opened ${filePath} via ${opener}`);
|
|
1233
1233
|
}
|
|
1234
|
+
// ── Word COM autosave sidecar (win32 + Word only) ──
|
|
1235
|
+
// "Save on close should be one step" (Bob 2026-07-30) — better: make
|
|
1236
|
+
// save CONTINUOUS so closing Word is the only step and the save
|
|
1237
|
+
// prompt never appears. A hidden PowerShell loop attaches to the
|
|
1238
|
+
// running Word via COM every ~2 s and calls doc.Save() whenever the
|
|
1239
|
+
// document is dirty. Each save trips the fs.watch above → mammoth →
|
|
1240
|
+
// compose mirrors the edit live. When the document (or Word) closes,
|
|
1241
|
+
// the sidecar exits on its own; stop() kills it if compose closes
|
|
1242
|
+
// first. PowerShell is used as the COM bridge deliberately: Node has
|
|
1243
|
+
// no COM without a native build dep, and allowScripts skips install
|
|
1244
|
+
// scripts so a compiled addon would arrive broken from the registry.
|
|
1245
|
+
let autosaveChild = null;
|
|
1246
|
+
if (process.platform === "win32" && opener === "word") {
|
|
1247
|
+
try {
|
|
1248
|
+
const psPath = path.join(dir, `${fileBase}-autosave.ps1`);
|
|
1249
|
+
fs.writeFileSync(psPath, [
|
|
1250
|
+
"param([string]$DocPath)",
|
|
1251
|
+
"$deadline = (Get-Date).AddHours(6)",
|
|
1252
|
+
"$attached = $false",
|
|
1253
|
+
"while ((Get-Date) -lt $deadline) {",
|
|
1254
|
+
" Start-Sleep -Seconds 2",
|
|
1255
|
+
" $word = $null",
|
|
1256
|
+
" try { $word = [Runtime.InteropServices.Marshal]::GetActiveObject('Word.Application') }",
|
|
1257
|
+
" catch { if ($attached) { exit 0 } else { continue } }",
|
|
1258
|
+
" $doc = $null",
|
|
1259
|
+
" try { foreach ($d in $word.Documents) { if ($d.FullName -ieq $DocPath) { $doc = $d; break } } } catch { continue }",
|
|
1260
|
+
" if ($null -eq $doc) { if ($attached) { exit 0 } else { continue } }",
|
|
1261
|
+
" $attached = $true",
|
|
1262
|
+
" try { if (-not $doc.Saved) { $doc.Save() } } catch { }",
|
|
1263
|
+
"}",
|
|
1264
|
+
"exit 1",
|
|
1265
|
+
].join("\r\n"), "utf-8");
|
|
1266
|
+
autosaveChild = spawn("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", psPath, filePath], { detached: false, stdio: "ignore", windowsHide: true });
|
|
1267
|
+
autosaveChild.on("error", () => { autosaveChild = null; });
|
|
1268
|
+
autosaveChild.on("exit", (code) => {
|
|
1269
|
+
console.log(` [word-edit] autosave sidecar exited (${code === 0 ? "document closed" : `code ${code}`})`);
|
|
1270
|
+
try {
|
|
1271
|
+
fs.unlinkSync(psPath);
|
|
1272
|
+
}
|
|
1273
|
+
catch { /* */ }
|
|
1274
|
+
});
|
|
1275
|
+
console.log(` [word-edit] autosave sidecar armed — closing Word is the only step`);
|
|
1276
|
+
}
|
|
1277
|
+
catch (e) {
|
|
1278
|
+
console.warn(` [word-edit] autosave sidecar failed to start (${e?.message || e}) — Ctrl+S still works`);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1234
1281
|
// Watch for save events. fs.watch on Windows fires multiple events
|
|
1235
1282
|
// per save (rename + change for atomic replacement); debounce so the
|
|
1236
1283
|
// UI only reloads once per save. Watch the directory rather than the
|
|
@@ -1305,6 +1352,10 @@ export class MailxService {
|
|
|
1305
1352
|
catch { /* */ }
|
|
1306
1353
|
if (debounce)
|
|
1307
1354
|
clearTimeout(debounce);
|
|
1355
|
+
try {
|
|
1356
|
+
autosaveChild?.kill();
|
|
1357
|
+
}
|
|
1358
|
+
catch { /* */ }
|
|
1308
1359
|
};
|
|
1309
1360
|
this.wordEdits.set(editId, { path: filePath, stop });
|
|
1310
1361
|
return { ok: opener !== "none", path: filePath, opener };
|
|
@@ -1767,20 +1818,118 @@ export class MailxService {
|
|
|
1767
1818
|
// counter. Without this, typing fast left N stale 90-folder IMAP
|
|
1768
1819
|
// sweeps all churning to completion in the background.
|
|
1769
1820
|
const myGen = ++this.serverSearchGen;
|
|
1770
|
-
// Parse qualifiers once; SEARCH runs per folder.
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1821
|
+
// Parse qualifiers once; SEARCH runs per folder. Same query
|
|
1822
|
+
// syntax as local search (Bob 2026-07-30 "IMAP search should be
|
|
1823
|
+
// the same as desktop"): quoted phrases, from:/to:/cc:/subject:,
|
|
1824
|
+
// is:unread etc., after:/before:/date:, uppercase NOT (works
|
|
1825
|
+
// anywhere, including first) and OR — all mapped onto IMAP's
|
|
1826
|
+
// native SEARCH keys (NOT/OR/UNSEEN/SINCE/… are core RFC 3501).
|
|
1827
|
+
// has:attachment has no IMAP key and is ignored server-side.
|
|
1828
|
+
const criteria = { not: [], or: [] };
|
|
1829
|
+
const bodyTerms = [];
|
|
1830
|
+
const unq = (s) => s.replace(/^"|"$/g, "");
|
|
1831
|
+
const parseRelDate = (s) => {
|
|
1832
|
+
const lower = s.toLowerCase().trim();
|
|
1833
|
+
if (lower === "today") {
|
|
1834
|
+
const d = new Date();
|
|
1835
|
+
d.setHours(0, 0, 0, 0);
|
|
1836
|
+
return d;
|
|
1837
|
+
}
|
|
1838
|
+
if (lower === "yesterday") {
|
|
1839
|
+
const d = new Date();
|
|
1840
|
+
d.setHours(0, 0, 0, 0);
|
|
1841
|
+
return new Date(d.getTime() - 86400_000);
|
|
1842
|
+
}
|
|
1843
|
+
const rel = lower.match(/^(\d+)([dwmy])$/);
|
|
1844
|
+
if (rel) {
|
|
1845
|
+
const n = parseInt(rel[1]);
|
|
1846
|
+
const ms = rel[2] === "d" ? n * 86400_000 : rel[2] === "w" ? n * 7 * 86400_000
|
|
1847
|
+
: rel[2] === "m" ? n * 30 * 86400_000 : n * 365 * 86400_000;
|
|
1848
|
+
return new Date(Date.now() - ms);
|
|
1849
|
+
}
|
|
1850
|
+
const ts = Date.parse(s);
|
|
1851
|
+
return isNaN(ts) ? null : new Date(ts);
|
|
1852
|
+
};
|
|
1853
|
+
// One token → one criteria fragment (or null for date/unmappable,
|
|
1854
|
+
// which merge into the top level / get dropped).
|
|
1855
|
+
const tokenToFrag = (tok) => {
|
|
1856
|
+
const m = tok.match(/^(from|to|cc|subject):(.+)$/i);
|
|
1857
|
+
if (m)
|
|
1858
|
+
return { [m[1].toLowerCase()]: unq(m[2]) };
|
|
1859
|
+
const is = tok.match(/^is:(.+)$/i);
|
|
1860
|
+
if (is) {
|
|
1861
|
+
const v = is[1].toLowerCase();
|
|
1862
|
+
if (v === "unread")
|
|
1863
|
+
return { unseen: true };
|
|
1864
|
+
if (v === "read" || v === "seen")
|
|
1865
|
+
return { seen: true };
|
|
1866
|
+
if (v === "flagged" || v === "starred")
|
|
1867
|
+
return { flagged: true };
|
|
1868
|
+
if (v === "answered")
|
|
1869
|
+
return { answered: true };
|
|
1870
|
+
if (v === "draft")
|
|
1871
|
+
return { draft: true };
|
|
1872
|
+
return null;
|
|
1873
|
+
}
|
|
1874
|
+
if (/^(has|folder|date|after|before):/i.test(tok))
|
|
1875
|
+
return null;
|
|
1876
|
+
const t = unq(tok);
|
|
1877
|
+
return t ? { body: t } : null;
|
|
1878
|
+
};
|
|
1879
|
+
const tokens = q.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
|
|
1880
|
+
let negate = false;
|
|
1881
|
+
for (let ti = 0; ti < tokens.length; ti++) {
|
|
1882
|
+
const tok = tokens[ti];
|
|
1883
|
+
if (tok === "NOT") {
|
|
1884
|
+
negate = true;
|
|
1885
|
+
continue;
|
|
1886
|
+
}
|
|
1887
|
+
if (tok === "AND" || tok === "OR") {
|
|
1888
|
+
negate = false;
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
// Dates apply at the top level (negated dates are rare and
|
|
1892
|
+
// ambiguous — treated as positive).
|
|
1893
|
+
const dm = tok.match(/^date:([><]?=?)(.+)$/i) || tok.match(/^(after|before):(.+)$/i);
|
|
1894
|
+
if (dm) {
|
|
1895
|
+
const isAfter = /^after:/i.test(tok) || dm[1] === ">" || dm[1] === ">=";
|
|
1896
|
+
const isBefore = /^before:/i.test(tok) || dm[1] === "<" || dm[1] === "<=";
|
|
1897
|
+
const d = parseRelDate(unq(dm[2]));
|
|
1898
|
+
if (d) {
|
|
1899
|
+
if (isAfter)
|
|
1900
|
+
criteria.since = d;
|
|
1901
|
+
else if (isBefore)
|
|
1902
|
+
criteria.before = d;
|
|
1903
|
+
else {
|
|
1904
|
+
criteria.since = d;
|
|
1905
|
+
criteria.before = new Date(d.getTime() + 86400_000);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
negate = false;
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1911
|
+
const frag = tokenToFrag(tok);
|
|
1912
|
+
if (!frag) {
|
|
1913
|
+
negate = false;
|
|
1914
|
+
continue;
|
|
1915
|
+
}
|
|
1916
|
+
const orAdjacent = tokens[ti - 1] === "OR" || tokens[ti + 1] === "OR";
|
|
1917
|
+
if (negate)
|
|
1918
|
+
criteria.not.push(frag);
|
|
1919
|
+
else if (orAdjacent)
|
|
1920
|
+
criteria.or.push(frag);
|
|
1921
|
+
else if (frag.body)
|
|
1922
|
+
bodyTerms.push(frag.body);
|
|
1923
|
+
else
|
|
1924
|
+
Object.assign(criteria, frag);
|
|
1925
|
+
negate = false;
|
|
1926
|
+
}
|
|
1927
|
+
if (bodyTerms.length)
|
|
1928
|
+
criteria.body = bodyTerms.length === 1 ? bodyTerms[0] : bodyTerms;
|
|
1929
|
+
if (criteria.not.length === 0)
|
|
1930
|
+
delete criteria.not;
|
|
1931
|
+
if (criteria.or.length === 0)
|
|
1932
|
+
delete criteria.or;
|
|
1784
1933
|
// Server search spans every selectable folder on every enabled
|
|
1785
1934
|
// account — otherwise a message that got moved / was in Sent /
|
|
1786
1935
|
// only exists in an archive folder silently fails to turn up.
|