@bobfrankston/mailx-settings 0.1.6 → 0.1.9
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/cloud.d.ts +29 -4
- package/cloud.d.ts.map +1 -0
- package/cloud.js +69 -49
- package/copy-docs.js +31 -0
- package/docs/accounts.md +46 -0
- package/docs/allowlist.md +27 -0
- package/docs/clients.md +24 -0
- package/docs/config.md +32 -0
- package/docs/contact-rules.md +40 -0
- package/docs/contacts.md +48 -0
- package/docs/editor.md +92 -0
- package/docs/preferences.md +43 -0
- package/index.d.ts +52 -5
- package/index.d.ts.map +1 -0
- package/index.js +365 -43
- package/package.json +13 -3
- package/tsconfig.tsbuildinfo +0 -1
package/cloud.d.ts
CHANGED
|
@@ -16,10 +16,35 @@
|
|
|
16
16
|
* Dropbox (removed 2026-04-06): Never implemented — placeholder only.
|
|
17
17
|
* Would need Dropbox OAuth app, token management, and Dropbox API v2 calls.
|
|
18
18
|
*/
|
|
19
|
-
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
/** Verify a cached folder ID still points to an `.rmfmail`-named folder
|
|
20
|
+
* the user owns and isn't trashed. Returns true if the ID is good to use,
|
|
21
|
+
* false if mailx should drop it and re-discover via gDriveFindOrCreateFolder.
|
|
22
|
+
*
|
|
23
|
+
* Catches the cross-version-corruption case: 1.0.504 had a buggy folder
|
|
24
|
+
* lookup that created an empty `rmfmail` folder at My Drive root and cached
|
|
25
|
+
* its ID. Once 1.0.505 fixed the lookup, the cached ID still pointed at the
|
|
26
|
+
* buggy folder forever — mailx kept reading the 1-account stub instead of
|
|
27
|
+
* the user's real `home/.rmfmail/accounts.jsonc`. This validator forces a
|
|
28
|
+
* re-discovery whenever the cached ID is wrong. */
|
|
29
|
+
export declare function gDriveValidateCachedFolder(folderId: string): Promise<boolean>;
|
|
30
|
+
/** Find or create the rmfmail settings folder on Google Drive.
|
|
31
|
+
*
|
|
32
|
+
* Lookup order — match the Android side (`mailx-store-web/android-bootstrap.ts`):
|
|
33
|
+
* 1. `home/.rmfmail/` — the established convention. Bob's family-of-two
|
|
34
|
+
* layout has been here since the rebrand. Path scope (`'root' in
|
|
35
|
+
* parents` for `home`, then `<homeId> in parents` for `.rmfmail`) is
|
|
36
|
+
* what the Android side does, so desktop matches.
|
|
37
|
+
* 2. `.rmfmail/` at My Drive root — fallback for users who don't have a
|
|
38
|
+
* `home/` folder. Mostly hypothetical; included so a brand-new Drive
|
|
39
|
+
* still works.
|
|
40
|
+
* 3. Create `.rmfmail` at My Drive root if neither exists. Don't auto-
|
|
41
|
+
* create `home/` — that's a user-organization choice we shouldn't
|
|
42
|
+
* make for them.
|
|
43
|
+
*
|
|
44
|
+
* Previously this looked for a literal `rmfmail` (no dot) at root, which
|
|
45
|
+
* on clean install created a NEW empty folder alongside the real data,
|
|
46
|
+
* orphaning the user's accounts/contacts/etc. Switched to the dotted name
|
|
47
|
+
* matching the actual folder convention. */
|
|
23
48
|
export declare function gDriveFindOrCreateFolder(): Promise<string | null>;
|
|
24
49
|
export type CloudProvider = "gdrive" | "google" | "local";
|
|
25
50
|
export interface CloudFile {
|
package/cloud.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["cloud.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AA0JH;;;;;;;;;oDASoD;AACpD,wBAAsB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA8BnF;AAcD;;;;;;;;;;;;;;;;;6CAiB6C;AAC7C,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAwBvE;AA4ED,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE1D,MAAM,WAAW,SAAS;IACtB,kFAAkF;IAClF,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/C,uGAAuG;IACvG,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,8BAA8B;IAC9B,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9C;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAuBtF;AAED;;;;2DAI2D;AAC3D,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAoBvG"}
|
package/cloud.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import fs from "node:fs";
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { authenticateOAuth } from "@bobfrankston/oauthsupport";
|
|
22
|
-
const SETTINGS_DIR = path.join(process.env.USERPROFILE || process.env.HOME || ".", ".
|
|
22
|
+
const SETTINGS_DIR = path.join(process.env.USERPROFILE || process.env.HOME || ".", ".rmfmail");
|
|
23
23
|
// ── Credentials ──
|
|
24
24
|
// Google Drive: reuse iflow's OAuth credentials (same Google Cloud project)
|
|
25
25
|
function findGoogleCredentials() {
|
|
@@ -54,10 +54,6 @@ const GDRIVE_TOKEN_DIR = path.join(SETTINGS_DIR, "tokens", "gdrive");
|
|
|
54
54
|
// The Gmail scope is already mail.google.com (full email), so this isn't a
|
|
55
55
|
// bigger privacy ask. Token cache at tokens/gdrive/ will re-auth on scope change.
|
|
56
56
|
const GDRIVE_SCOPES = "https://www.googleapis.com/auth/drive";
|
|
57
|
-
/** Paths to try when no config.path is set (fresh install). Order matters:
|
|
58
|
-
* "home/.mailx" is the user convention for shared family settings; "mailx"
|
|
59
|
-
* is the auto-created default. First one that exists on GDrive wins. */
|
|
60
|
-
const GDRIVE_PATH_SEARCH_ORDER = ["home/.mailx", "mailx"];
|
|
61
57
|
// ── Token helpers ──
|
|
62
58
|
/** Get a GDrive-capable token. Prefers the Gmail token (which now includes
|
|
63
59
|
* drive scope) — avoids a second OAuth consent prompt. Falls back to the
|
|
@@ -103,7 +99,7 @@ async function getGoogleDriveToken() {
|
|
|
103
99
|
// Strategy 2: dedicated GDrive token (legacy path or non-Gmail setups)
|
|
104
100
|
const creds = findGoogleCredentials();
|
|
105
101
|
if (!creds) {
|
|
106
|
-
console.error(" [cloud] No Google credentials found (checked ~/.
|
|
102
|
+
console.error(" [cloud] No Google credentials found (checked ~/.rmfmail/google-credentials.json and iflow package)");
|
|
107
103
|
return null;
|
|
108
104
|
}
|
|
109
105
|
const tokenPath = path.join(GDRIVE_TOKEN_DIR, "token.json");
|
|
@@ -179,19 +175,46 @@ async function walkGDrivePath(token, segments, create) {
|
|
|
179
175
|
}
|
|
180
176
|
return parentId || null;
|
|
181
177
|
}
|
|
182
|
-
/**
|
|
183
|
-
|
|
178
|
+
/** Verify a cached folder ID still points to an `.rmfmail`-named folder
|
|
179
|
+
* the user owns and isn't trashed. Returns true if the ID is good to use,
|
|
180
|
+
* false if mailx should drop it and re-discover via gDriveFindOrCreateFolder.
|
|
181
|
+
*
|
|
182
|
+
* Catches the cross-version-corruption case: 1.0.504 had a buggy folder
|
|
183
|
+
* lookup that created an empty `rmfmail` folder at My Drive root and cached
|
|
184
|
+
* its ID. Once 1.0.505 fixed the lookup, the cached ID still pointed at the
|
|
185
|
+
* buggy folder forever — mailx kept reading the 1-account stub instead of
|
|
186
|
+
* the user's real `home/.rmfmail/accounts.jsonc`. This validator forces a
|
|
187
|
+
* re-discovery whenever the cached ID is wrong. */
|
|
188
|
+
export async function gDriveValidateCachedFolder(folderId) {
|
|
189
|
+
const token = await getGoogleDriveToken();
|
|
190
|
+
if (!token)
|
|
191
|
+
return true; // can't verify → trust the cache (no token = nothing else to do)
|
|
184
192
|
try {
|
|
185
|
-
const res = await fetch(`https://www.googleapis.com/drive/v3/files/${folderId}?fields=name,
|
|
186
|
-
|
|
187
|
-
|
|
193
|
+
const res = await fetch(`https://www.googleapis.com/drive/v3/files/${folderId}?fields=id,name,trashed,mimeType`, { headers: { Authorization: `Bearer ${token}` } });
|
|
194
|
+
if (res.status === 404) {
|
|
195
|
+
console.log(` [cloud] cached folderId ${folderId} → 404 (folder deleted), re-discovering`);
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
188
198
|
if (!res.ok)
|
|
189
|
-
return
|
|
199
|
+
return true;
|
|
190
200
|
const data = await res.json();
|
|
191
|
-
|
|
201
|
+
if (data.trashed) {
|
|
202
|
+
console.log(` [cloud] cached folderId ${folderId} is trashed, re-discovering`);
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
if (data.mimeType !== "application/vnd.google-apps.folder") {
|
|
206
|
+
console.log(` [cloud] cached folderId ${folderId} is not a folder (${data.mimeType}), re-discovering`);
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
if (data.name !== ".rmfmail") {
|
|
210
|
+
console.log(` [cloud] cached folderId ${folderId} is named '${data.name}', not '.rmfmail' — re-discovering`);
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
192
214
|
}
|
|
193
|
-
catch {
|
|
194
|
-
|
|
215
|
+
catch (e) {
|
|
216
|
+
console.error(` [cloud] gDriveValidateCachedFolder: ${e.message}`);
|
|
217
|
+
return true; // network failure → trust the cache
|
|
195
218
|
}
|
|
196
219
|
}
|
|
197
220
|
/** Find a single folder by name, optionally inside a parent. */
|
|
@@ -209,48 +232,45 @@ async function gDriveFindFolder(token, name, parentId) {
|
|
|
209
232
|
const data = await res.json();
|
|
210
233
|
return data.files?.[0]?.id || null;
|
|
211
234
|
}
|
|
212
|
-
/** Find the settings folder on
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
235
|
+
/** Find or create the rmfmail settings folder on Google Drive.
|
|
236
|
+
*
|
|
237
|
+
* Lookup order — match the Android side (`mailx-store-web/android-bootstrap.ts`):
|
|
238
|
+
* 1. `home/.rmfmail/` — the established convention. Bob's family-of-two
|
|
239
|
+
* layout has been here since the rebrand. Path scope (`'root' in
|
|
240
|
+
* parents` for `home`, then `<homeId> in parents` for `.rmfmail`) is
|
|
241
|
+
* what the Android side does, so desktop matches.
|
|
242
|
+
* 2. `.rmfmail/` at My Drive root — fallback for users who don't have a
|
|
243
|
+
* `home/` folder. Mostly hypothetical; included so a brand-new Drive
|
|
244
|
+
* still works.
|
|
245
|
+
* 3. Create `.rmfmail` at My Drive root if neither exists. Don't auto-
|
|
246
|
+
* create `home/` — that's a user-organization choice we shouldn't
|
|
247
|
+
* make for them.
|
|
248
|
+
*
|
|
249
|
+
* Previously this looked for a literal `rmfmail` (no dot) at root, which
|
|
250
|
+
* on clean install created a NEW empty folder alongside the real data,
|
|
251
|
+
* orphaning the user's accounts/contacts/etc. Switched to the dotted name
|
|
252
|
+
* matching the actual folder convention. */
|
|
216
253
|
export async function gDriveFindOrCreateFolder() {
|
|
217
254
|
const token = await getGoogleDriveToken();
|
|
218
255
|
if (!token)
|
|
219
256
|
return null;
|
|
220
|
-
// Read path from config — supports nested like "home/.mailx"
|
|
221
|
-
let cfgEntry = null;
|
|
222
257
|
try {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
258
|
+
// 1. Try home/.rmfmail (the canonical location)
|
|
259
|
+
const inHome = await walkGDrivePath(token, ["home", ".rmfmail"], false);
|
|
260
|
+
if (inHome) {
|
|
261
|
+
console.log(` [cloud] Found 'home/.rmfmail' folder (${inHome})`);
|
|
262
|
+
return inHome;
|
|
227
263
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
? [configuredPath]
|
|
234
|
-
: GDRIVE_PATH_SEARCH_ORDER;
|
|
235
|
-
try {
|
|
236
|
-
for (const tryPath of pathsToTry) {
|
|
237
|
-
console.log(` [cloud] Trying GDrive path: '${tryPath}'`);
|
|
238
|
-
const segments = tryPath.split(/[/\\]/).filter(Boolean);
|
|
239
|
-
const folderId = await walkGDrivePath(token, segments, false);
|
|
240
|
-
if (folderId) {
|
|
241
|
-
// Resolve folder ID back to name for verification
|
|
242
|
-
const name = await gDriveGetFolderName(token, folderId);
|
|
243
|
-
console.log(` [cloud] Found existing '${tryPath}' → folder '${name || "?"}' (${folderId})`);
|
|
244
|
-
return folderId;
|
|
245
|
-
}
|
|
246
|
-
console.log(` [cloud] Path '${tryPath}' not found on GDrive`);
|
|
264
|
+
// 2. Try .rmfmail at root
|
|
265
|
+
const atRoot = await walkGDrivePath(token, [".rmfmail"], false);
|
|
266
|
+
if (atRoot) {
|
|
267
|
+
console.log(` [cloud] Found '.rmfmail' folder at My Drive root (${atRoot})`);
|
|
268
|
+
return atRoot;
|
|
247
269
|
}
|
|
248
|
-
//
|
|
249
|
-
const
|
|
250
|
-
const segments = createPath.split(/[/\\]/).filter(Boolean);
|
|
251
|
-
const created = await walkGDrivePath(token, segments, true);
|
|
270
|
+
// 3. Create .rmfmail at root (don't try to create `home/`)
|
|
271
|
+
const created = await walkGDrivePath(token, [".rmfmail"], true);
|
|
252
272
|
if (created)
|
|
253
|
-
console.log(` [cloud] Created '
|
|
273
|
+
console.log(` [cloud] Created '.rmfmail' folder at My Drive root (${created})`);
|
|
254
274
|
return created;
|
|
255
275
|
}
|
|
256
276
|
catch (e) {
|
package/copy-docs.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* copy-docs.js — pre-pack hook for mailx-settings.
|
|
4
|
+
*
|
|
5
|
+
* The reference docs live at `<workspace>/app/docs/*.md` so they're easy
|
|
6
|
+
* to edit and discover in the source tree. The published npm package
|
|
7
|
+
* needs them as a sibling of index.js so `deployDocs()` can find them at
|
|
8
|
+
* runtime via `__dirname/docs/`. This script copies the workspace docs
|
|
9
|
+
* into a local `./docs/` directory just before npm publish, then npm's
|
|
10
|
+
* `files` field includes them in the tarball.
|
|
11
|
+
*
|
|
12
|
+
* Idempotent — safe to run multiple times. Source path is the workspace
|
|
13
|
+
* root resolved upward from this package.
|
|
14
|
+
*/
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
|
|
18
|
+
const __dirname = import.meta.dirname;
|
|
19
|
+
const src = path.join(__dirname, "..", "..", "docs");
|
|
20
|
+
const dst = path.join(__dirname, "docs");
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(src)) {
|
|
23
|
+
console.log(`[mailx-settings] no source docs at ${src} — skipping copy`);
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
27
|
+
const files = fs.readdirSync(src).filter(f => f.endsWith(".md"));
|
|
28
|
+
for (const f of files) {
|
|
29
|
+
fs.copyFileSync(path.join(src, f), path.join(dst, f));
|
|
30
|
+
}
|
|
31
|
+
console.log(`[mailx-settings] copied ${files.length} .md file(s) from app/docs/ → docs/`);
|
package/docs/accounts.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# accounts.jsonc
|
|
2
|
+
|
|
3
|
+
> **Owned by rmfmail. Do not edit this file — your changes will be overwritten the next time the app starts.** The canonical copy ships with each release; this file is a deployed copy for reference. To change account *settings*, edit `accounts.jsonc` (which IS user-editable). To change *documentation*, file an issue.
|
|
4
|
+
|
|
5
|
+
## What this file documents
|
|
6
|
+
|
|
7
|
+
`accounts.jsonc` lists every email account rmfmail manages. It lives in your shared GDrive folder (`My Drive/home/.rmfmail/`) so all your devices read the same list.
|
|
8
|
+
|
|
9
|
+
## Shape
|
|
10
|
+
|
|
11
|
+
```jsonc
|
|
12
|
+
{
|
|
13
|
+
"name": "Bob Frankston", // optional; default name applied to every account that doesn't override
|
|
14
|
+
"accounts": [
|
|
15
|
+
{
|
|
16
|
+
"id": "gmail", // short tag used in folder paths and the UI
|
|
17
|
+
"email": "you@example.com", // primary login address
|
|
18
|
+
"name": "Bob Frankston", // display name (optional if file-level name is set)
|
|
19
|
+
"imap": { "host": "imap.example.com", "port": 993, "tls": true, "auth": "password" },
|
|
20
|
+
"smtp": { "host": "smtp.example.com", "port": 465, "tls": true, "auth": "password" },
|
|
21
|
+
"enabled": true, // false skips this account at startup
|
|
22
|
+
"identityDomains": ["alias.com"] // extra domains for Reply-From auto-detect
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"keys": { // AI provider API keys (optional)
|
|
26
|
+
"anthropic": "", // sk-ant-api03-...
|
|
27
|
+
"openai": "" // sk-...
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Field rules
|
|
33
|
+
|
|
34
|
+
- **id** — short identifier. Used in folder paths (e.g. `bob.ma/INBOX`) and as a stable key when accounts.jsonc is edited.
|
|
35
|
+
- **email** — primary login. Determines provider auto-config when `imap`/`smtp` are omitted.
|
|
36
|
+
- **imap / smtp** — explicit server config. Omit for known providers (Gmail / Outlook / Yahoo / iCloud / Google Workspace are detected from the email domain via MX records).
|
|
37
|
+
- **auth** — `"password"` for traditional IMAP/SMTP, `"oauth2"` for Gmail/Google Workspace/Outlook.
|
|
38
|
+
- **enabled** — set `false` to keep the account record but skip sync at startup.
|
|
39
|
+
- **identityDomains** — addresses you receive at on alternative domains. When you Reply, rmfmail picks the matching identity address as From instead of the account's primary.
|
|
40
|
+
- **keys.anthropic / keys.openai** — API keys for the AI features (translate / proofread / summarize / autocomplete). Lives at the file's top level alongside `accounts:` so you set it once across all your devices. Generated at console.anthropic.com or platform.openai.com. Provider selection is in `preferences.jsonc`; the key here is read based on which provider is active. Empty string means "not configured" — the AI feature silently no-ops. Local Ollama provider needs no key.
|
|
41
|
+
|
|
42
|
+
## Notes
|
|
43
|
+
|
|
44
|
+
- JSONC: `// line comments` and trailing commas are allowed. The parser is lenient.
|
|
45
|
+
- Adding/removing accounts requires a daemon restart (`rmfmail -kill && rmfmail`). A status banner reminds you when the file changes.
|
|
46
|
+
- For OAuth accounts (Gmail, Google Workspace), the token cache lives in `~/.rmfmail/tokens/` and is per-machine.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# allowlist.jsonc
|
|
2
|
+
|
|
3
|
+
> **Owned by rmfmail. Do not edit this file — your changes will be overwritten the next time the app starts.** This is reference documentation. To change allow-list data, edit `allowlist.jsonc` itself.
|
|
4
|
+
|
|
5
|
+
## What this file documents
|
|
6
|
+
|
|
7
|
+
`allowlist.jsonc` controls which senders' remote content (images, etc.) loads automatically and which senders/domains are flagged with a ⚠ banner. Lives in `My Drive/home/.rmfmail/`.
|
|
8
|
+
|
|
9
|
+
## Shape
|
|
10
|
+
|
|
11
|
+
```jsonc
|
|
12
|
+
{
|
|
13
|
+
"senders": ["alice@example.com"], // load remote content for these senders
|
|
14
|
+
"domains": ["example.com"], // load remote content for any sender at these domains
|
|
15
|
+
"recipients": ["you@example.com"], // addresses you receive at — treated as "your inbox identity"
|
|
16
|
+
"flaggedSenders": ["spam@bad.com"], // viewer shows ⚠ FLAGGED banner for these senders
|
|
17
|
+
"flaggedDomains": ["phishing.example"] // viewer shows ⚠ FLAGGED banner for any sender at these domains
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Notes
|
|
22
|
+
|
|
23
|
+
- All entries are case-insensitive plain email or domain strings.
|
|
24
|
+
- Multi-client safe: each device's save merges with the cloud copy (set-union), so adds from any device propagate.
|
|
25
|
+
- Use the message viewer's "allow remote content" button to add a sender/domain to `senders`/`domains` interactively.
|
|
26
|
+
- Use the right-click menu in the viewer to flag a sender/domain — adds to `flaggedSenders`/`flaggedDomains`.
|
|
27
|
+
- JSONC: `// line comments` and trailing commas are allowed.
|
package/docs/clients.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# clients.jsonc
|
|
2
|
+
|
|
3
|
+
> **Owned by rmfmail. Do not edit this file — your changes will be overwritten the next time the app starts.** This is reference documentation.
|
|
4
|
+
|
|
5
|
+
## What this file documents
|
|
6
|
+
|
|
7
|
+
`clients.jsonc` is the registry of devices running rmfmail under your account. It's auto-managed: each device adds itself on first launch and refreshes its `lastSeen` timestamp on every startup. You don't normally need to look at it.
|
|
8
|
+
|
|
9
|
+
## Shape
|
|
10
|
+
|
|
11
|
+
```jsonc
|
|
12
|
+
{
|
|
13
|
+
"devices": [
|
|
14
|
+
{ "id": "abc-uuid", "name": "Pixel 9 Pro Fold", "lastSeen": 1746381600000, "accounts": ["gmail", "bobma"] },
|
|
15
|
+
{ "id": "def-uuid", "name": "rmf39 desktop", "lastSeen": 1746399999000, "accounts": ["gmail", "bobma"] }
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Notes
|
|
21
|
+
|
|
22
|
+
- `id` is a stable UUID generated on first launch and stored locally (`localStorage` on Android, config dir on desktop).
|
|
23
|
+
- `accounts` is the list of account ids this device is currently configured for.
|
|
24
|
+
- Removing a device from this file forces it to re-register on next launch — harmless but pointless.
|
package/docs/config.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# config.jsonc
|
|
2
|
+
|
|
3
|
+
> **Owned by rmfmail. Do not edit this DOCUMENTATION file — changes will be overwritten.** Note: the **actual** `config.jsonc` IS user-editable; this `.md` file is just the reference for it.
|
|
4
|
+
|
|
5
|
+
## What this file documents
|
|
6
|
+
|
|
7
|
+
`config.jsonc` is your *local, per-machine* config. It is **NOT** synced to GDrive — every machine has its own. It points at the shared GDrive folder and overrides machine-specific paths.
|
|
8
|
+
|
|
9
|
+
Lives at `~/.rmfmail/config.jsonc` on the local filesystem.
|
|
10
|
+
|
|
11
|
+
## Shape
|
|
12
|
+
|
|
13
|
+
```jsonc
|
|
14
|
+
{
|
|
15
|
+
"sharedDir": {
|
|
16
|
+
"provider": "gdrive",
|
|
17
|
+
"path": ".rmfmail", // folder name in My Drive (currently My Drive/home/.rmfmail)
|
|
18
|
+
"folderId": "1AbCdEf...XYZ" // resolved Drive folderId, cached after first lookup
|
|
19
|
+
},
|
|
20
|
+
"storePath": "~/.rmfmail/mailxstore", // local directory for .eml message bodies
|
|
21
|
+
"historyDays": 30 // how far back to sync; overrides the shared default
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Notes
|
|
26
|
+
|
|
27
|
+
- `sharedDir.provider`: `"gdrive"` is the only currently-supported value (OneDrive/Dropbox were removed).
|
|
28
|
+
- `sharedDir.folderId` is resolved at startup if missing; caching it avoids a Drive query on every launch.
|
|
29
|
+
- `storePath` is where `.eml` bodies are stored locally. Defaults to `~/.rmfmail/mailxstore` on desktop. Android uses app-private sandbox storage and ignores this field.
|
|
30
|
+
- `historyDays` is a per-machine override for the cloud-synced default in `preferences.jsonc`.
|
|
31
|
+
- Because this file is local, edits don't propagate to other devices. Use `accounts.jsonc` / `preferences.jsonc` for shared settings.
|
|
32
|
+
- JSONC: `// line comments` and trailing commas are allowed.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# contact-rules.jsonc
|
|
2
|
+
|
|
3
|
+
> **Owned by rmfmail. Do not edit this file — your changes will be overwritten the next time the app starts.** This is reference documentation. The actual rules ship with the app and update on every release.
|
|
4
|
+
|
|
5
|
+
## What this file documents
|
|
6
|
+
|
|
7
|
+
`contact-rules.jsonc` defines the **global** junk-contact filter patterns (noreply, mailer-daemon, gateway domains, one-off-shape locals, etc.). It is *NOT* user-editable — it's part of the app, deployed alongside your data so you can see what's being filtered.
|
|
8
|
+
|
|
9
|
+
User-specific filters live in `contacts.jsonc` (`denylist` exact match; `denylistPatterns` regex array — TODO).
|
|
10
|
+
|
|
11
|
+
## Shape
|
|
12
|
+
|
|
13
|
+
```jsonc
|
|
14
|
+
{
|
|
15
|
+
"rulesVersion": "v3-domain-oneoff", // bump when patterns tighten — triggers one-shot purge
|
|
16
|
+
"junk": {
|
|
17
|
+
"localExact": "^(no-?reply|do-?not-?reply|noreply|mailer-daemon|...)$",
|
|
18
|
+
"localSuffix": "(-bounces|\\+bounces|-noreply|-no-reply|...)$",
|
|
19
|
+
"localPrefix": "^(no-?reply|noreply|notifications?|alerts?|bounces?|mailer)[-_+]",
|
|
20
|
+
"localOneoff": "^[0-9a-f]{4,}\\.[0-9a-f]{4,}(\\.[0-9a-z]{6,})?$",
|
|
21
|
+
"domain": "^(txt\\.voice\\.google\\.com|reply\\.facebook\\.com|reply\\.linkedin\\.com)$"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## How rules apply
|
|
27
|
+
|
|
28
|
+
Each newly-harvested address is checked against ALL rule patterns; any match drops it. Each rule is a single regex, case-insensitive:
|
|
29
|
+
|
|
30
|
+
- **localExact** — whole-string match against the local part (before `@`).
|
|
31
|
+
- **localSuffix** — local part ends with the pattern.
|
|
32
|
+
- **localPrefix** — local part begins with the pattern + a `-` / `_` / `+` separator. Catches `noreply-<random>` style.
|
|
33
|
+
- **localOneoff** — full-shape match for per-message gateway addresses (Google Voice SMS uses three dot-separated alphanumeric segments).
|
|
34
|
+
- **domain** — whole-string match against the domain (after `@`).
|
|
35
|
+
|
|
36
|
+
## When rules update
|
|
37
|
+
|
|
38
|
+
Each rmfmail release ships an updated `contact-rules.jsonc`. On startup, the app compares the bundled `rulesVersion` to the value stored locally. If they differ, the app sweeps the contacts table once and removes rows the new rules now reject — then records the new version so the sweep doesn't repeat.
|
|
39
|
+
|
|
40
|
+
To add a rule: file an issue, or fork+PR. (Eventually `rules.jsonc` per-user will allow personal additions without forking.)
|
package/docs/contacts.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# contacts.jsonc
|
|
2
|
+
|
|
3
|
+
> **Owned by rmfmail. Do not edit this file — your changes will be overwritten the next time the app starts.** This is reference documentation. To change contact data, edit `contacts.jsonc` itself.
|
|
4
|
+
|
|
5
|
+
## What this file documents
|
|
6
|
+
|
|
7
|
+
`contacts.jsonc` is your address book. It's split into three tiers and lives in `My Drive/home/.rmfmail/`.
|
|
8
|
+
|
|
9
|
+
## Shape
|
|
10
|
+
|
|
11
|
+
```jsonc
|
|
12
|
+
{
|
|
13
|
+
"preferred": [
|
|
14
|
+
{ "name": "David Reed", "email": "dpreed@example.com", "organization": "Deep Plum" }
|
|
15
|
+
],
|
|
16
|
+
"denylist": [
|
|
17
|
+
"spammer@example.com"
|
|
18
|
+
],
|
|
19
|
+
"discovered": [
|
|
20
|
+
{ "name": "Bob", "email": "bob@example.com", "useCount": 715, "lastUsed": 1777921829000 }
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Tiers
|
|
26
|
+
|
|
27
|
+
- **preferred** — manually-curated, top-ranked in autocomplete. Add people you actually want to reach quickly.
|
|
28
|
+
- **denylist** — addresses (lowercased) excluded from autocomplete entirely.
|
|
29
|
+
- **discovered** — auto-collected from sent/received mail. `useCount` × recency drives autocomplete ranking. Junk patterns (see below) are dropped at insertion.
|
|
30
|
+
|
|
31
|
+
## Global junk filter
|
|
32
|
+
|
|
33
|
+
Addresses matching any of these are dropped at insertion AND swept on startup:
|
|
34
|
+
|
|
35
|
+
- Whole local part: `noreply`, `no-reply`, `do-not-reply`, `mailer-daemon`, `postmaster`, `bounces`, etc.
|
|
36
|
+
- Local-part prefix: `noreply-<random>`, `notifications-<id>`, `alerts-<x>`, `bounces-<x>`, `mailer-<x>`
|
|
37
|
+
- Local-part suffix: `*-bounces`, `*+bounces`, `*-noreply`, `*-notifications`
|
|
38
|
+
- Multi-segment one-off shape: `16179691997.15082868100.nfblcbll1x` (per-message gateway addresses)
|
|
39
|
+
- Domains: `txt.voice.google.com` (Google Voice SMS gateway), `reply.facebook.com`, `reply.linkedin.com`
|
|
40
|
+
|
|
41
|
+
The full pattern set lives in `contact-rules.jsonc` (shipped with the app). Bumping `rulesVersion` in that file triggers a one-shot purge of historical rows the new rules now reject.
|
|
42
|
+
|
|
43
|
+
## Notes
|
|
44
|
+
|
|
45
|
+
- JSONC: `// line comments` and trailing commas are allowed.
|
|
46
|
+
- `useCount` and `lastUsed` are auto-managed; manually editing them only changes ranking briefly until the next sync.
|
|
47
|
+
- Removing a `preferred` entry doesn't add it to `denylist` — it just demotes back to `discovered` (or drops if not seen in mail).
|
|
48
|
+
- Each device contributes its observed addresses to `discovered`. The file accumulates the union across devices.
|
package/docs/editor.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Compose editor — formatting and shortcuts
|
|
2
|
+
|
|
3
|
+
mailx ships with two rich-text editors: **Quill** (default) and **tiptap**.
|
|
4
|
+
Most things work the same in both. Differences are called out below.
|
|
5
|
+
|
|
6
|
+
Switch editors via **Settings → Editor → Quill | tiptap**.
|
|
7
|
+
|
|
8
|
+
## Universal — works in both editors
|
|
9
|
+
|
|
10
|
+
| Action | Shortcut | Where |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| **Send** | Ctrl+Enter | toolbar Send button |
|
|
13
|
+
| Bold / Italic / Underline | Ctrl+B / Ctrl+I / Ctrl+U | toolbar B / I / U |
|
|
14
|
+
| Strikethrough | Ctrl+Shift+X | toolbar S |
|
|
15
|
+
| Bulleted list | Ctrl+Shift+8 | toolbar • |
|
|
16
|
+
| Ordered list | Ctrl+Shift+7 | toolbar 1. |
|
|
17
|
+
| Insert / edit link | Ctrl+K | toolbar 🔗 |
|
|
18
|
+
| Remove link | Ctrl+Shift+K | (no toolbar button — use Ctrl+Shift+K) |
|
|
19
|
+
| Blockquote | (Quill: Ctrl+Shift+Q; tiptap: toolbar) | toolbar `“` |
|
|
20
|
+
| Clear formatting | Ctrl+\\ | toolbar ✖ |
|
|
21
|
+
| Heading (H1 / H2 / H3) | (Quill: format dropdown; tiptap: select dropdown) | "Normal / Heading 1 / 2 / 3" |
|
|
22
|
+
| Undo / Redo | Ctrl+Z / Ctrl+Y | (no toolbar button) |
|
|
23
|
+
| Spell-check | (browser native — red underlines) | right-click word |
|
|
24
|
+
| Paste plain text | Ctrl+Shift+V | (browser native) |
|
|
25
|
+
|
|
26
|
+
## Quill-only
|
|
27
|
+
|
|
28
|
+
| Action | Shortcut |
|
|
29
|
+
|---|---|
|
|
30
|
+
| Indent / outdent | Ctrl+] / Ctrl+[ |
|
|
31
|
+
| Color text | Ctrl+Shift+C |
|
|
32
|
+
| Format dropdown | toolbar (left side) |
|
|
33
|
+
| Inline code | toolbar `<>` |
|
|
34
|
+
| Code block | toolbar |
|
|
35
|
+
| Image inserter | (no built-in; use drag-and-drop or paste) |
|
|
36
|
+
|
|
37
|
+
Quill has a more elaborate toolbar with format-specific dropdowns (font,
|
|
38
|
+
size, color). Internally Quill uses its own *Delta* document model — copy/
|
|
39
|
+
paste from Word/Outlook sometimes leaves extra `<p><br></p>` empty
|
|
40
|
+
paragraphs that you'll see in the sent message body.
|
|
41
|
+
|
|
42
|
+
## tiptap-only
|
|
43
|
+
|
|
44
|
+
| Action | Where |
|
|
45
|
+
|---|---|
|
|
46
|
+
| Heading select | left of toolbar |
|
|
47
|
+
| Toggle bold / italic / underline / strike | toolbar B / I / U / S |
|
|
48
|
+
| Blockquote | toolbar `“` |
|
|
49
|
+
| Image (drag-and-drop) | drop a file into the body |
|
|
50
|
+
|
|
51
|
+
tiptap is built on ProseMirror. Output HTML is cleaner than Quill on
|
|
52
|
+
Word/Outlook paste roundtrips. Bundle is smaller. Some Quill toolbar
|
|
53
|
+
features (inline code, indent shortcuts, color picker) aren't wired —
|
|
54
|
+
use the heading select / format menu instead.
|
|
55
|
+
|
|
56
|
+
## Common features (across both)
|
|
57
|
+
|
|
58
|
+
- **Drag-and-drop attachments** — drop files anywhere on the compose
|
|
59
|
+
window to attach. Overlay highlights the drop target while dragging.
|
|
60
|
+
- **Edit in Word / LibreOffice** — toolbar **Edit in Word** button opens
|
|
61
|
+
the body in your default external editor (configurable in
|
|
62
|
+
Settings → External editor). Save in the external editor and the body
|
|
63
|
+
reloads here. Behind the scenes mailx writes a temporary `.docx` file
|
|
64
|
+
(see `~/.rmfmail/external-edit/`) and watches it for changes.
|
|
65
|
+
- **Auto-save drafts** — every 5 seconds (and on input debounce + on
|
|
66
|
+
blur). Drafts land in the Drafts folder via IMAP append.
|
|
67
|
+
- **Address auto-completion** — type a partial name in To/Cc/Bcc; matches
|
|
68
|
+
rank by recency × use-count. Group names from `contacts.jsonc → groups`
|
|
69
|
+
also surface here.
|
|
70
|
+
- **Address-field expansion** — recipient fields are auto-growing
|
|
71
|
+
textareas; long lists wrap to multiple lines.
|
|
72
|
+
- **Group expansion on send** — type a group name (e.g. `family`) in
|
|
73
|
+
To/Cc/Bcc and it expands to the address list at send time.
|
|
74
|
+
- **Ghost-text autocomplete** (off by default) — Settings →
|
|
75
|
+
AI autocomplete → on. Predicts the next words while you type.
|
|
76
|
+
|
|
77
|
+
## When the toolbar doesn't appear
|
|
78
|
+
|
|
79
|
+
The editor loads from a CDN (jsdelivr). If your network can't reach it,
|
|
80
|
+
the toolbar disappears and a plain `contenteditable` fallback takes over.
|
|
81
|
+
Status bar shows the failure. mailx now tries the *other* editor before
|
|
82
|
+
falling all the way back; if both fail you get a plain textarea with no
|
|
83
|
+
shortcuts and no toolbar — sending still works.
|
|
84
|
+
|
|
85
|
+
## See also
|
|
86
|
+
|
|
87
|
+
- `accounts.md`, `contacts.md`, `allowlist.md`, `clients.md`, `config.md`,
|
|
88
|
+
`preferences.md` — config-file references (these live in your GDrive
|
|
89
|
+
`.rmfmail/` folder).
|
|
90
|
+
- This `editor.md` is **app-internal** — it ships with each release and
|
|
91
|
+
documents the editor as it currently exists in the version you're
|
|
92
|
+
running. It is not deployed to your user folder.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# preferences.jsonc
|
|
2
|
+
|
|
3
|
+
> **Owned by rmfmail. Do not edit this file — your changes will be overwritten the next time the app starts.** This is reference documentation.
|
|
4
|
+
|
|
5
|
+
## What this file documents
|
|
6
|
+
|
|
7
|
+
`preferences.jsonc` holds your shared UI / sync / autocomplete preferences, synced via `My Drive/home/.rmfmail/`.
|
|
8
|
+
|
|
9
|
+
## Shape
|
|
10
|
+
|
|
11
|
+
```jsonc
|
|
12
|
+
{
|
|
13
|
+
"ui": {
|
|
14
|
+
"theme": "system", // "system" | "light" | "dark"
|
|
15
|
+
"twoLine": false, // two-line message-list rows
|
|
16
|
+
"previewPane": true,
|
|
17
|
+
"previewSnippets": true,
|
|
18
|
+
"threaded": false,
|
|
19
|
+
"flaggedOnly": false,
|
|
20
|
+
"folderCounts": false,
|
|
21
|
+
"calendarSidebar": false,
|
|
22
|
+
"editor": "quill" // "quill" | "tiptap"
|
|
23
|
+
},
|
|
24
|
+
"sync": {
|
|
25
|
+
"intervalMinutes": 5, // full-sync interval (per-folder); IDLE handles INBOX inline
|
|
26
|
+
"historyDays": 30, // how far back to sync (overridable per-machine in config.jsonc)
|
|
27
|
+
"prefetch": true // background-fetch message bodies after metadata sync
|
|
28
|
+
},
|
|
29
|
+
"autocomplete": {
|
|
30
|
+
"enabled": false, // ghost-text completions in compose
|
|
31
|
+
"provider": "ollama", // "ollama" | "claude" | "openai"
|
|
32
|
+
"translateEnabled": false, // AI-powered Translate menu item in viewer
|
|
33
|
+
"proofreadEnabled": false
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Notes
|
|
39
|
+
|
|
40
|
+
- The View menu and Settings menu in the toolbar both write here.
|
|
41
|
+
- `sync.intervalMinutes` controls the full-folder sync cadence. INBOX uses RFC 2177 IDLE (instant push) on Dovecot accounts; Gmail accounts poll every 30s.
|
|
42
|
+
- `sync.historyDays` is a default; override per-machine via `config.jsonc`.
|
|
43
|
+
- AI features are off by default. Enabling `autocomplete.enabled` requires a configured provider (Ollama local, Claude API key, or OpenAI API key).
|