@bobfrankston/mailx-settings 0.1.19 → 0.1.21
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/docs/contacts.md +17 -8
- package/docs/rmf-tiny.md +31 -2
- package/index.d.ts +5 -4
- package/index.d.ts.map +1 -1
- package/index.js +66 -16
- package/package.json +3 -3
package/docs/contacts.md
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
## What this file documents
|
|
6
6
|
|
|
7
|
-
`contacts.jsonc` is your address book
|
|
7
|
+
`contacts.jsonc` is the **curated** part of your address book — the entries you (or the app on your behalf) explicitly keep. It lives in your shared `.rmfmail` folder on Google Drive and syncs across devices.
|
|
8
|
+
|
|
9
|
+
Auto-collected addresses (the *discovered* set) are **not** stored in this file — they are a per-device local cache. See [Discovered addresses](#discovered-addresses) below.
|
|
8
10
|
|
|
9
11
|
## Shape
|
|
10
12
|
|
|
@@ -16,9 +18,6 @@
|
|
|
16
18
|
"denylist": [
|
|
17
19
|
"spammer@example.com"
|
|
18
20
|
],
|
|
19
|
-
"discovered": [
|
|
20
|
-
{ "name": "Bob", "email": "bob@example.com", "useCount": 715, "lastUsed": 1777921829000 }
|
|
21
|
-
],
|
|
22
21
|
"groups": {
|
|
23
22
|
"Joes": ["joe@example.com", "jo@example.com"],
|
|
24
23
|
"Family": ["alice@example.com", "Joes"]
|
|
@@ -30,9 +29,20 @@
|
|
|
30
29
|
|
|
31
30
|
- **preferred** — manually-curated, top-ranked in autocomplete. Add people you actually want to reach quickly.
|
|
32
31
|
- **denylist** — addresses (lowercased) excluded from autocomplete entirely.
|
|
33
|
-
- **discovered** — auto-collected from sent/received mail. `useCount` × recency drives autocomplete ranking. Junk patterns (see below) are dropped at insertion.
|
|
34
32
|
- **groups** — mailing lists. Type the group name in To/Cc/Bcc and compose expands it into the member addresses on send. Each value is an array of either email addresses (`"a@b.com"` or `"Name <a@b.com>"`) or other group names (recursive aliasing — cycles are detected, depth capped at 10). Group lookup is case-insensitive on the key. The two `Joes` example above plus the `Family` example show flat membership and one group nested inside another.
|
|
35
33
|
|
|
34
|
+
Other keys may also appear (e.g. `priorityDomains`) — rmfmail merges into the existing file rather than overwriting it, so hand-added keys survive a flush.
|
|
35
|
+
|
|
36
|
+
## Discovered addresses
|
|
37
|
+
|
|
38
|
+
Addresses seen in your sent/received mail are collected automatically into a **discovered** set. This set:
|
|
39
|
+
|
|
40
|
+
- is a **local cache** held in each device's database — it is *not* written to `contacts.jsonc` and *not* synced to Google Drive;
|
|
41
|
+
- drives autocomplete ranking via `useCount` × recency;
|
|
42
|
+
- drops addresses matching the junk patterns below, both at insertion and on a startup sweep.
|
|
43
|
+
|
|
44
|
+
Keeping it local keeps `contacts.jsonc` a few KB of curated data instead of a multi-megabyte blob, and avoids cross-device clobbering on every use-count tick. Each device builds its own discovered set from its own copy of your mail.
|
|
45
|
+
|
|
36
46
|
## Global junk filter
|
|
37
47
|
|
|
38
48
|
Addresses matching any of these are dropped at insertion AND swept on startup:
|
|
@@ -48,6 +58,5 @@ The full pattern set lives in `contact-rules.jsonc` (shipped with the app). Bump
|
|
|
48
58
|
## Notes
|
|
49
59
|
|
|
50
60
|
- JSONC: `// line comments` and trailing commas are allowed.
|
|
51
|
-
- `
|
|
52
|
-
-
|
|
53
|
-
- Each device contributes its observed addresses to `discovered`. The file accumulates the union across devices.
|
|
61
|
+
- Removing a `preferred` entry doesn't add it to `denylist` — it just demotes back to the discovered set (or drops if not seen in mail).
|
|
62
|
+
- `preferred` and `denylist` are the only tiers this file owns; `groups` and other keys are preserved on flush but otherwise hand-managed.
|
package/docs/rmf-tiny.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# `rmf-tiny` —
|
|
1
|
+
# `rmf-tiny` — TinyMCE editor adapter for rmfmail
|
|
2
2
|
|
|
3
|
-
Status:
|
|
3
|
+
Status: **implemented and shipping.** Source: `y:/dev/utils/msgx/libs/rmf-tiny/src/adapter.ts`; built copy served as `client/lib/rmf-tiny.js` (via `bin/build-rmf-tiny.js`). Selectable in Settings → Compose → Editor. The design notes below are kept for history; see **Known TinyMCE quirks** at the bottom for the live gotcha list.
|
|
4
4
|
|
|
5
5
|
## Goal
|
|
6
6
|
|
|
@@ -154,3 +154,32 @@ Don't publish the adapter until the Word-paste case actually works end-to-end th
|
|
|
154
154
|
- The adapter must not re-implement parts of TinyMCE — it's a thin wrapper. Re-implementing parts of GPL code in MIT is the legal trap; staying purely in adapter / interface territory is safe.
|
|
155
155
|
- TinyMCE's bundle is large (~1MB minified). Users opting in pay that cost on the first compose-window load. Fine for opt-in; would be unacceptable as a default.
|
|
156
156
|
- `rmf-tiny` major version should track TinyMCE's major version (rmf-tiny@7.x for tinymce@7.x) so peerDependency mismatches are obvious.
|
|
157
|
+
|
|
158
|
+
## Known TinyMCE quirks
|
|
159
|
+
|
|
160
|
+
TinyMCE frequently gets caret / scroll / block behaviour wrong. Each entry
|
|
161
|
+
below is a quirk found in use and the workaround in `adapter.ts`. Add to
|
|
162
|
+
this list whenever a new one turns up — it's the running gotcha log.
|
|
163
|
+
|
|
164
|
+
- **Viewport doesn't follow `setCursor`.** After `setContent` of a long
|
|
165
|
+
reply, collapsing the selection to the start (caret at top, where the
|
|
166
|
+
user types above the quote) does NOT scroll the iframe — it can be left
|
|
167
|
+
scrolled down inside the quoted block. Fix: `setCursor(0)` explicitly
|
|
168
|
+
calls `editor.getWin().scrollTo(0, 0)`; non-zero positions call
|
|
169
|
+
`editor.selection.scrollIntoView()`. (Bob 2026-05-18.)
|
|
170
|
+
|
|
171
|
+
- **Typing at a link's trailing edge extends the link.** With the caret
|
|
172
|
+
collapsed at the end of an `<a>`, contenteditable keeps appending typed
|
|
173
|
+
characters INTO the link — a whole sentence becomes link text. Fix: a
|
|
174
|
+
`keypress` handler hops the caret to just after the `<a>` first.
|
|
175
|
+
|
|
176
|
+
- **Native spellcheck is force-enabled.** `adapter.ts`'s `init` handler
|
|
177
|
+
sets `spellcheck="true"` on the body. mailx runs its own nspell checker
|
|
178
|
+
and overrides this back to `"false"` in `wireSpellcheck` (with a
|
|
179
|
+
MutationObserver) so the two don't double-underline.
|
|
180
|
+
|
|
181
|
+
- **`<p>` collapses to `<br>`.** Enter / paste sometimes produces a `<br>`
|
|
182
|
+
line break where a `<p>` paragraph was expected, so paragraph spacing is
|
|
183
|
+
lost until the lines are merged and Enter re-pressed. Not yet fixed —
|
|
184
|
+
candidate: `forced_root_block` / paste-normalization config. (Bob
|
|
185
|
+
2026-05-18, exact trigger still unclear.)
|
package/index.d.ts
CHANGED
|
@@ -154,10 +154,11 @@ export declare function loadAllowlist(): typeof DEFAULT_ALLOWLIST;
|
|
|
154
154
|
/** Save allow-list — merges with existing cloud copy (multi-client safe) */
|
|
155
155
|
export declare function saveAllowlist(list: typeof DEFAULT_ALLOWLIST): Promise<void>;
|
|
156
156
|
/** Load user-added dictionary words. Mirrored to GDrive so "Add to dictionary"
|
|
157
|
-
* on one machine appears on every machine.
|
|
158
|
-
*
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
* on one machine appears on every machine. Returns a deduped string array.
|
|
158
|
+
* Tries the cloud copy first (so a fresh machine sees prior words), then
|
|
159
|
+
* the local cache. Empty array when nothing has been saved yet. */
|
|
160
|
+
export declare function loadUserDict(): Promise<string[]>;
|
|
161
|
+
/** Save user dictionary — merges with the cloud copy so concurrent edits on
|
|
161
162
|
* different machines union rather than overwrite. */
|
|
162
163
|
export declare function saveUserDict(words: string[]): Promise<void>;
|
|
163
164
|
/** Load settings — unified view combining all files (backward compatible) */
|
package/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"}
|
package/index.js
CHANGED
|
@@ -1047,29 +1047,79 @@ export async function saveAllowlist(list) {
|
|
|
1047
1047
|
catch { /* cloud read failed — save local version */ }
|
|
1048
1048
|
saveFile("allowlist.jsonc", merged);
|
|
1049
1049
|
}
|
|
1050
|
+
// User dictionary lives in a plain CSV (single column, one word per line)
|
|
1051
|
+
// so it's trivially hand-editable on the server. `# ...` lines and blanks
|
|
1052
|
+
// are ignored.
|
|
1053
|
+
const USER_DICT_FILE = "userdict.csv";
|
|
1054
|
+
/** Parse the single-column user-dictionary CSV. Blank lines and `#` comment
|
|
1055
|
+
* lines are skipped; a quoted or multi-field row contributes its first
|
|
1056
|
+
* field — so a file exported from a spreadsheet still loads cleanly. */
|
|
1057
|
+
function parseDictCsv(text) {
|
|
1058
|
+
return text.split(/\r?\n/)
|
|
1059
|
+
.map(l => l.trim())
|
|
1060
|
+
.filter(l => l && !l.startsWith("#"))
|
|
1061
|
+
.map(l => l.split(",")[0].trim().replace(/^"(.*)"$/, "$1").trim())
|
|
1062
|
+
.filter(Boolean);
|
|
1063
|
+
}
|
|
1064
|
+
/** Serialize words to CSV text — sorted, one per line, with a header note. */
|
|
1065
|
+
function buildDictCsv(words) {
|
|
1066
|
+
const sorted = [...new Set(words)].filter(Boolean).sort((a, b) => a.localeCompare(b));
|
|
1067
|
+
return "# rmfmail user dictionary — one word per line\n" + sorted.join("\n") + "\n";
|
|
1068
|
+
}
|
|
1069
|
+
function atomicWriteText(filePath, text) {
|
|
1070
|
+
const dir = path.dirname(filePath);
|
|
1071
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1072
|
+
const tmp = filePath + ".tmp";
|
|
1073
|
+
fs.writeFileSync(tmp, text);
|
|
1074
|
+
fs.renameSync(tmp, filePath);
|
|
1075
|
+
}
|
|
1050
1076
|
/** Load user-added dictionary words. Mirrored to GDrive so "Add to dictionary"
|
|
1051
|
-
* on one machine appears on every machine.
|
|
1052
|
-
*
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1077
|
+
* on one machine appears on every machine. Returns a deduped string array.
|
|
1078
|
+
* Tries the cloud copy first (so a fresh machine sees prior words), then
|
|
1079
|
+
* the local cache. Empty array when nothing has been saved yet. */
|
|
1080
|
+
export async function loadUserDict() {
|
|
1081
|
+
try {
|
|
1082
|
+
const cloudCsv = await cloudRead(USER_DICT_FILE);
|
|
1083
|
+
if (cloudCsv != null)
|
|
1084
|
+
return [...new Set(parseDictCsv(cloudCsv))];
|
|
1085
|
+
}
|
|
1086
|
+
catch { /* cloud unreachable — fall back to local */ }
|
|
1087
|
+
const sharedPath = path.join(getSharedDir(), USER_DICT_FILE);
|
|
1088
|
+
const localPath = path.join(LOCAL_DIR, USER_DICT_FILE);
|
|
1089
|
+
try {
|
|
1090
|
+
if (fs.existsSync(sharedPath))
|
|
1091
|
+
return [...new Set(parseDictCsv(fs.readFileSync(sharedPath, "utf-8")))];
|
|
1092
|
+
if (fs.existsSync(localPath))
|
|
1093
|
+
return [...new Set(parseDictCsv(fs.readFileSync(localPath, "utf-8")))];
|
|
1094
|
+
}
|
|
1095
|
+
catch { /* unreadable */ }
|
|
1096
|
+
return [];
|
|
1056
1097
|
}
|
|
1057
|
-
/** Save user dictionary — merges with cloud copy so concurrent edits on
|
|
1098
|
+
/** Save user dictionary — merges with the cloud copy so concurrent edits on
|
|
1058
1099
|
* different machines union rather than overwrite. */
|
|
1059
1100
|
export async function saveUserDict(words) {
|
|
1060
|
-
|
|
1101
|
+
const merged = new Set(words);
|
|
1061
1102
|
try {
|
|
1062
|
-
const
|
|
1063
|
-
if (
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
merged = [...new Set([...merged, ...cloud.words])];
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1103
|
+
const cloudCsv = await cloudRead(USER_DICT_FILE);
|
|
1104
|
+
if (cloudCsv)
|
|
1105
|
+
for (const w of parseDictCsv(cloudCsv))
|
|
1106
|
+
merged.add(w);
|
|
1069
1107
|
}
|
|
1070
1108
|
catch { /* cloud unreachable — save local version */ }
|
|
1071
|
-
merged
|
|
1072
|
-
|
|
1109
|
+
const text = buildDictCsv([...merged]);
|
|
1110
|
+
const sharedDir = getSharedDir();
|
|
1111
|
+
atomicWriteText(path.join(sharedDir, USER_DICT_FILE), text);
|
|
1112
|
+
if (sharedDir !== LOCAL_DIR) {
|
|
1113
|
+
try {
|
|
1114
|
+
atomicWriteText(path.join(LOCAL_DIR, USER_DICT_FILE), text);
|
|
1115
|
+
}
|
|
1116
|
+
catch { /* ignore */ }
|
|
1117
|
+
}
|
|
1118
|
+
if (pendingCloudConfig) {
|
|
1119
|
+
cloudWrite(USER_DICT_FILE, text)
|
|
1120
|
+
.then(() => console.log(` [cloud] Saved ${USER_DICT_FILE} via ${pendingCloudConfig.provider} API`))
|
|
1121
|
+
.catch(e => console.error(` [cloud] Failed to save ${USER_DICT_FILE}: ${e.message}`));
|
|
1122
|
+
}
|
|
1073
1123
|
}
|
|
1074
1124
|
// ── Legacy compatibility ──
|
|
1075
1125
|
function loadLegacySettings() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-settings",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"license": "ISC",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@bobfrankston/mailx-types": "^0.1.
|
|
20
|
+
"@bobfrankston/mailx-types": "^0.1.18",
|
|
21
21
|
"jsonc-parser": "^3.3.1"
|
|
22
22
|
},
|
|
23
23
|
"repository": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
".transformedSnapshot": {
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@bobfrankston/mailx-types": "^0.1.
|
|
36
|
+
"@bobfrankston/mailx-types": "^0.1.18",
|
|
37
37
|
"jsonc-parser": "^3.3.1"
|
|
38
38
|
}
|
|
39
39
|
}
|