@bobfrankston/mailx 1.0.23 → 1.0.24
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/README.md +135 -138
- package/package.json +1 -1
- package/packages/mailx-server/index.js +7 -0
- package/packages/mailx-settings/index.js +33 -1
package/README.md
CHANGED
|
@@ -6,130 +6,144 @@ A local-first email client with IMAP sync, full offline reading, and a standalon
|
|
|
6
6
|
|
|
7
7
|
MIT License -- Copyright (c) 2026 Bob Frankston
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Installation
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npm
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
launch.ps1 # Or use the native WebView2 app
|
|
16
|
-
launch.ps1 -restart # Kill existing server and restart
|
|
12
|
+
npm install -g @bobfrankston/mailx
|
|
13
|
+
mailx
|
|
17
14
|
```
|
|
18
15
|
|
|
19
|
-
|
|
16
|
+
Requires Node.js 22 or later (uses built-in `node:sqlite`).
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
On Windows, the native WebView2 app launches automatically. On Linux/Mac, it opens in your default browser at `http://127.0.0.1:9333`.
|
|
22
19
|
|
|
23
|
-
|
|
20
|
+
## First-Time Setup
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"storePath": "C:/Users/You/.mailx/mailxstore"
|
|
29
|
-
}
|
|
30
|
-
```
|
|
22
|
+
### Option A: Existing user on a new machine (shared settings on OneDrive)
|
|
23
|
+
|
|
24
|
+
If you already have mailx configured on another machine with settings on OneDrive, just install and run. mailx auto-detects shared settings at:
|
|
31
25
|
|
|
32
|
-
-
|
|
33
|
-
-
|
|
26
|
+
- `%OneDrive%/home/.mailx/settings.jsonc`
|
|
27
|
+
- `%OneDriveConsumer%/home/.mailx/settings.jsonc`
|
|
28
|
+
- `~/OneDrive/home/.mailx/settings.jsonc`
|
|
34
29
|
|
|
35
|
-
|
|
30
|
+
Once OneDrive syncs, mailx picks up your accounts automatically.
|
|
36
31
|
|
|
37
|
-
###
|
|
32
|
+
### Option B: New user -- set up from scratch
|
|
33
|
+
|
|
34
|
+
**Step 1.** Create the settings directory:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
mkdir ~/.mailx
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
(On Windows: `mkdir %USERPROFILE%\.mailx`)
|
|
41
|
+
|
|
42
|
+
**Step 2.** Create `~/.mailx/settings.jsonc` with your account(s):
|
|
38
43
|
|
|
39
44
|
```jsonc
|
|
40
45
|
{
|
|
41
46
|
"accounts": [
|
|
42
47
|
{
|
|
43
|
-
"id": "
|
|
44
|
-
"name": "
|
|
45
|
-
"
|
|
48
|
+
"id": "mymail", // Internal ID (no spaces, lowercase)
|
|
49
|
+
"name": "Your Name", // Sender name in From: header
|
|
50
|
+
"label": "Work", // Display label in UI (optional, defaults to name)
|
|
51
|
+
"email": "you@example.com", // Your email address
|
|
46
52
|
"imap": {
|
|
47
|
-
"host": "imap.example.com",
|
|
48
|
-
"port": 993,
|
|
53
|
+
"host": "imap.example.com", // IMAP server
|
|
54
|
+
"port": 993, // Usually 993 for SSL/TLS
|
|
49
55
|
"tls": true,
|
|
50
|
-
"auth": "password",
|
|
51
|
-
"user": "
|
|
52
|
-
"password": "
|
|
56
|
+
"auth": "password", // "password" or "oauth2" (Gmail)
|
|
57
|
+
"user": "you@example.com", // IMAP username
|
|
58
|
+
"password": "your-password" // IMAP password (not needed for oauth2)
|
|
53
59
|
},
|
|
54
60
|
"smtp": {
|
|
55
|
-
"host": "smtp.example.com",
|
|
56
|
-
"port": 587,
|
|
61
|
+
"host": "smtp.example.com", // SMTP server
|
|
62
|
+
"port": 587, // Usually 587 for STARTTLS
|
|
57
63
|
"tls": true,
|
|
58
64
|
"auth": "password",
|
|
59
|
-
"user": "
|
|
60
|
-
"password": "
|
|
65
|
+
"user": "you@example.com",
|
|
66
|
+
"password": "your-password"
|
|
61
67
|
},
|
|
62
|
-
"enabled": true
|
|
63
|
-
|
|
64
|
-
{
|
|
65
|
-
"id": "gmail",
|
|
66
|
-
"name": "Gmail",
|
|
67
|
-
"email": "bob@gmail.com",
|
|
68
|
-
"imap": {
|
|
69
|
-
"host": "imap.gmail.com", "port": 993, "tls": true,
|
|
70
|
-
"auth": "oauth2", "user": "bob@gmail.com"
|
|
71
|
-
},
|
|
72
|
-
"smtp": {
|
|
73
|
-
"host": "smtp.gmail.com", "port": 587, "tls": true,
|
|
74
|
-
"auth": "oauth2", "user": "bob@gmail.com"
|
|
75
|
-
},
|
|
76
|
-
"enabled": true
|
|
68
|
+
"enabled": true,
|
|
69
|
+
"defaultSend": true // Use this account for sending when From doesn't match
|
|
77
70
|
}
|
|
78
71
|
],
|
|
79
72
|
"sync": {
|
|
80
|
-
"intervalMinutes": 5,
|
|
81
|
-
"historyDays":
|
|
82
|
-
},
|
|
83
|
-
// UI preferences
|
|
84
|
-
"ui": {
|
|
85
|
-
"theme": "system", // "system" (follows OS), "dark", or "light"
|
|
86
|
-
"folderWidth": 220,
|
|
87
|
-
"listViewerSplit": 40,
|
|
88
|
-
"fontSize": 15
|
|
89
|
-
},
|
|
90
|
-
// Auto-populated when you click "Always allow from sender/domain"
|
|
91
|
-
"remoteAllowList": {
|
|
92
|
-
"senders": [],
|
|
93
|
-
"domains": []
|
|
73
|
+
"intervalMinutes": 5, // Full sync interval (minutes)
|
|
74
|
+
"historyDays": 0 // Days of history to sync (0 = all)
|
|
94
75
|
}
|
|
95
76
|
}
|
|
96
77
|
```
|
|
97
78
|
|
|
98
|
-
|
|
79
|
+
**Step 3.** Run `mailx` and open `http://127.0.0.1:9333` in your browser.
|
|
80
|
+
|
|
81
|
+
### Option C: Multi-machine with shared settings (OneDrive/Dropbox)
|
|
82
|
+
|
|
83
|
+
Put `settings.jsonc` on a cloud-synced folder, then create a local pointer:
|
|
84
|
+
|
|
85
|
+
**`~/.mailx/config.jsonc`:**
|
|
86
|
+
```jsonc
|
|
87
|
+
{
|
|
88
|
+
// Point to shared settings on OneDrive (or Dropbox, network share, etc.)
|
|
89
|
+
"sharedDir": "C:/Users/You/OneDrive/home/.mailx",
|
|
90
|
+
// Local-only: where cached messages are stored
|
|
91
|
+
"storePath": "C:/Users/You/.mailx/mailxstore",
|
|
92
|
+
// Per-machine override: days of history (0 = all)
|
|
93
|
+
"historyDays": 90
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The shared directory contains: `settings.jsonc`, `allowlist.jsonc`, `preferences.jsonc`. Each machine caches them locally for offline use.
|
|
99
98
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
### Adding a Gmail account
|
|
100
|
+
|
|
101
|
+
Gmail requires OAuth2 instead of a password:
|
|
102
|
+
|
|
103
|
+
1. Go to [Google Cloud Console](https://console.cloud.google.com/) and create a project
|
|
104
|
+
2. Enable the **Gmail API** (and **People API** for contacts)
|
|
105
|
+
3. Create **OAuth 2.0 credentials** (Desktop app type)
|
|
103
106
|
4. Download `credentials.json` to the iflow package directory
|
|
104
|
-
5.
|
|
107
|
+
5. Add the Gmail account to `settings.jsonc` with `"auth": "oauth2"`
|
|
108
|
+
6. First connection opens a browser for OAuth consent. Tokens are cached and refresh automatically.
|
|
109
|
+
|
|
110
|
+
```jsonc
|
|
111
|
+
{
|
|
112
|
+
"id": "gmail",
|
|
113
|
+
"name": "Your Name",
|
|
114
|
+
"label": "Gmail",
|
|
115
|
+
"email": "you@gmail.com",
|
|
116
|
+
"imap": { "host": "imap.gmail.com", "port": 993, "tls": true, "auth": "oauth2", "user": "you@gmail.com" },
|
|
117
|
+
"smtp": { "host": "smtp.gmail.com", "port": 587, "tls": true, "auth": "oauth2", "user": "you@gmail.com" },
|
|
118
|
+
"enabled": true
|
|
119
|
+
}
|
|
120
|
+
```
|
|
105
121
|
|
|
106
122
|
## Usage
|
|
107
123
|
|
|
108
124
|
### Reading Mail
|
|
109
|
-
- **Folder tree** (left)
|
|
110
|
-
- **Message list** (center)
|
|
111
|
-
- **Preview pane** (right)
|
|
112
|
-
- **All Inboxes**
|
|
125
|
+
- **Folder tree** (left) -- click to view, expand/collapse subfolders
|
|
126
|
+
- **Message list** (center) -- click to preview, scroll for more
|
|
127
|
+
- **Preview pane** (right) -- shows message content
|
|
128
|
+
- **All Inboxes** -- unified view across all accounts
|
|
129
|
+
- **Folder search** -- filter box above folder tree to find folders quickly
|
|
130
|
+
- **Message search** -- search bar with scope: This folder / All folders / IMAP server
|
|
113
131
|
|
|
114
132
|
### Composing
|
|
115
|
-
- **Compose**
|
|
116
|
-
- **Reply / Reply All / Forward**
|
|
117
|
-
- **Send**
|
|
118
|
-
- **From field**
|
|
119
|
-
- **Address autocomplete**
|
|
120
|
-
- **Auto-save**
|
|
133
|
+
- **Compose** -- toolbar or Ctrl+N
|
|
134
|
+
- **Reply / Reply All / Forward** -- Ctrl+R / Ctrl+Shift+R / toolbar
|
|
135
|
+
- **Send** -- Ctrl+Enter
|
|
136
|
+
- **From field** -- dropdown with all accounts + "Other..." for custom addresses
|
|
137
|
+
- **Address autocomplete** -- type in To/Cc/Bcc, Tab accepts match
|
|
138
|
+
- **Auto-save** -- drafts saved to IMAP every 5 seconds
|
|
121
139
|
|
|
122
140
|
### Managing Messages
|
|
123
|
-
- **Delete**
|
|
124
|
-
- **
|
|
125
|
-
- **
|
|
126
|
-
- **
|
|
127
|
-
- **
|
|
128
|
-
|
|
129
|
-
### View Options (View menu)
|
|
130
|
-
- **Two-line view** — From+Date on line 1, Subject on line 2
|
|
131
|
-
- **Preview pane** — toggle on/off
|
|
132
|
-
- **Flagged only** — show only starred messages
|
|
141
|
+
- **Delete** -- Del key or trash button, moves to Trash. Ctrl+Z to undo (30s).
|
|
142
|
+
- **Flag/unflag** -- click the star
|
|
143
|
+
- **Drag and drop** -- drag messages to folders to move them. Shift/Ctrl+click for multi-select.
|
|
144
|
+
- **Remote content** -- blocked by default. "Load once" or "Always" buttons on the banner.
|
|
145
|
+
- **Unsubscribe** -- button appears in header when List-Unsubscribe header is present
|
|
146
|
+
- **View Source** -- Source button copies .eml file path to clipboard
|
|
133
147
|
|
|
134
148
|
### Keyboard Shortcuts
|
|
135
149
|
| Key | Action |
|
|
@@ -139,31 +153,14 @@ If `config.jsonc` doesn't exist, settings default to `~/.mailx/settings.jsonc`.
|
|
|
139
153
|
| Ctrl+Shift+R | Reply All |
|
|
140
154
|
| Del / Ctrl+D | Delete |
|
|
141
155
|
| Ctrl+Z | Undo delete |
|
|
142
|
-
| Ctrl+K | Address completion |
|
|
143
156
|
| Ctrl+Enter | Send (compose) |
|
|
144
|
-
| Escape |
|
|
145
|
-
| F5 | Sync |
|
|
146
|
-
|
|
147
|
-
## Installation
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
npm install -g @bobfrankston/mailx
|
|
151
|
-
mailx # Starts server + opens browser
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
Or for development:
|
|
155
|
-
```bash
|
|
156
|
-
git clone https://github.com/BobFrankston/mailx.git
|
|
157
|
-
cd mailx
|
|
158
|
-
npm install
|
|
159
|
-
npm start # Starts server with --watch (auto-restart on changes)
|
|
160
|
-
```
|
|
157
|
+
| Escape | Clear search / Discard compose |
|
|
158
|
+
| F5 | Sync all folders |
|
|
161
159
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
```
|
|
160
|
+
### View Options (View menu)
|
|
161
|
+
- **Two-line view** -- compact message rows
|
|
162
|
+
- **Preview pane** -- toggle on/off
|
|
163
|
+
- **Flagged only** -- show only starred messages
|
|
167
164
|
|
|
168
165
|
## Data Storage
|
|
169
166
|
|
|
@@ -171,47 +168,47 @@ All data lives in `~/.mailx/` (e.g., `C:\Users\You\.mailx\`):
|
|
|
171
168
|
|
|
172
169
|
| File | Shared? | Purpose |
|
|
173
170
|
|------|---------|---------|
|
|
174
|
-
| config.jsonc | No | Points to shared settings
|
|
171
|
+
| config.jsonc | No | Points to shared settings + local overrides |
|
|
172
|
+
| settings.jsonc | Yes | Account configs, sync, UI prefs (or split files below) |
|
|
175
173
|
| accounts.jsonc | Yes | IMAP/SMTP account configs |
|
|
176
|
-
| preferences.jsonc | Yes | UI
|
|
174
|
+
| preferences.jsonc | Yes | UI and sync settings |
|
|
177
175
|
| allowlist.jsonc | Yes | Remote content sender/domain allow-list |
|
|
178
|
-
|
|
|
179
|
-
| mailx.db | No | SQLite — headers, contacts, sync state |
|
|
176
|
+
| mailx.db | No | SQLite metadata (headers, contacts, sync state) |
|
|
180
177
|
| mailxstore/ | No | Cached message bodies (.eml per message) |
|
|
178
|
+
| logs/ | No | Server logs (auto-deleted after 7 days) |
|
|
181
179
|
| window.json | No | Window position (per machine) |
|
|
182
|
-
|
|
|
180
|
+
| webview2/ | No | WebView2 data (Windows native app) |
|
|
183
181
|
|
|
184
|
-
**Shared** files can live on OneDrive/Dropbox
|
|
182
|
+
**Shared** files can live on OneDrive/Dropbox. **Local** files stay on the machine.
|
|
185
183
|
|
|
186
184
|
### Safe to Delete
|
|
187
185
|
|
|
188
|
-
- **mailxstore/**
|
|
189
|
-
- **mailx.db**
|
|
190
|
-
- **
|
|
191
|
-
- **window.json**
|
|
186
|
+
- **mailxstore/** -- re-downloaded during sync. Delete to reclaim space.
|
|
187
|
+
- **mailx.db** -- re-created on startup, triggers full re-sync.
|
|
188
|
+
- **logs/** -- safe to delete anytime.
|
|
189
|
+
- **window.json** -- resets window position.
|
|
192
190
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
```json
|
|
196
|
-
{
|
|
197
|
-
"sharedDir": "C:/Users/You/OneDrive/home/.mailx",
|
|
198
|
-
"storePath": "C:/Users/You/.mailx/mailxstore",
|
|
199
|
-
"historyDays": 90
|
|
200
|
-
}
|
|
201
|
-
```
|
|
191
|
+
## Development
|
|
202
192
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
193
|
+
```bash
|
|
194
|
+
git clone https://github.com/BobFrankston/mailx.git
|
|
195
|
+
cd mailx
|
|
196
|
+
npm install
|
|
197
|
+
npm start # Server with --watch (auto-restart)
|
|
198
|
+
# Open http://127.0.0.1:9333
|
|
206
199
|
|
|
207
|
-
|
|
200
|
+
launch.ps1 # Windows native WebView2 app
|
|
201
|
+
launch.ps1 -restart # Kill existing and restart
|
|
202
|
+
launch.ps1 -clear # Clear WebView2 cache
|
|
203
|
+
```
|
|
208
204
|
|
|
209
205
|
## Architecture
|
|
210
206
|
|
|
211
|
-
- **Local-first**
|
|
212
|
-
- **Offline reading**
|
|
213
|
-
- **IMAP IDLE**
|
|
214
|
-
- **Outbox**
|
|
215
|
-
- **Remote content blocking**
|
|
216
|
-
- **Search**
|
|
217
|
-
- **
|
|
207
|
+
- **Local-first** -- changes update local DB immediately, background worker syncs to IMAP
|
|
208
|
+
- **Offline reading** -- full message bodies cached as .eml files during sync
|
|
209
|
+
- **IMAP IDLE** -- instant new mail notifications + periodic sync fallback
|
|
210
|
+
- **Outbox** -- queued in IMAP Outbox folder with multi-machine interlock via flags
|
|
211
|
+
- **Remote content blocking** -- HTML sanitized server-side, CSP in iframe, per-sender/domain allow-list
|
|
212
|
+
- **Search** -- SQLite FTS5 full-text index, IMAP server search, regex filtering
|
|
213
|
+
- **Built-in SQLite** -- uses Node.js built-in node:sqlite (no native compilation needed)
|
|
214
|
+
- **Cross-platform** -- Windows (WebView2), Linux (webkit2gtk), any platform (browser mode)
|
package/package.json
CHANGED
|
@@ -48,6 +48,13 @@ const CLIENT_VERSION = clientPkg.version;
|
|
|
48
48
|
// ── Initialize ──
|
|
49
49
|
initLocalConfig();
|
|
50
50
|
const settings = loadSettings();
|
|
51
|
+
if (settings.accounts.length === 0) {
|
|
52
|
+
console.log(" No accounts configured.");
|
|
53
|
+
console.log(" See README for setup: https://github.com/BobFrankston/mailx#first-time-setup");
|
|
54
|
+
console.log(" Quick: create ~/.mailx/settings.jsonc with your IMAP/SMTP account details.");
|
|
55
|
+
console.log(" Or: place shared settings on OneDrive at home/.mailx/settings.jsonc");
|
|
56
|
+
console.log(" Server will start at http://127.0.0.1:9333 — configure accounts to begin.");
|
|
57
|
+
}
|
|
51
58
|
const dbDir = getConfigDir();
|
|
52
59
|
const db = new MailxDB(dbDir);
|
|
53
60
|
const imapManager = new ImapManager(db);
|
|
@@ -205,16 +205,48 @@ export function getConfigDir() {
|
|
|
205
205
|
}
|
|
206
206
|
/** Get the shared settings directory */
|
|
207
207
|
export { getSharedDir };
|
|
208
|
+
/** Auto-detect shared settings on OneDrive or common cloud sync locations */
|
|
209
|
+
function detectSharedDir() {
|
|
210
|
+
const home = process.env.USERPROFILE || process.env.HOME || "";
|
|
211
|
+
const candidates = [
|
|
212
|
+
// OneDrive paths (Windows)
|
|
213
|
+
process.env.OneDrive && path.join(process.env.OneDrive, "home", ".mailx"),
|
|
214
|
+
process.env.OneDriveConsumer && path.join(process.env.OneDriveConsumer, "home", ".mailx"),
|
|
215
|
+
home && path.join(home, "OneDrive", "home", ".mailx"),
|
|
216
|
+
// Linux/Mac OneDrive
|
|
217
|
+
home && path.join(home, "OneDrive", "home", ".mailx"),
|
|
218
|
+
// Dropbox
|
|
219
|
+
home && path.join(home, "Dropbox", ".mailx"),
|
|
220
|
+
// Local fallback — just use ~/.mailx itself
|
|
221
|
+
].filter(Boolean);
|
|
222
|
+
for (const dir of candidates) {
|
|
223
|
+
if (fs.existsSync(path.join(dir, "settings.jsonc")) || fs.existsSync(path.join(dir, "accounts.jsonc"))) {
|
|
224
|
+
return dir;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
208
229
|
/** Initialize local config if it doesn't exist */
|
|
209
230
|
export function initLocalConfig(sharedDir, storePath) {
|
|
210
231
|
if (fs.existsSync(LOCAL_CONFIG_PATH) && !sharedDir && !storePath)
|
|
211
232
|
return;
|
|
212
233
|
const existing = readLocalConfig();
|
|
234
|
+
// Auto-detect shared settings if not configured
|
|
235
|
+
let resolvedSharedDir = sharedDir || existing.sharedDir;
|
|
236
|
+
if (!resolvedSharedDir && existing.settingsPath) {
|
|
237
|
+
resolvedSharedDir = path.dirname(existing.settingsPath);
|
|
238
|
+
}
|
|
239
|
+
if (!resolvedSharedDir) {
|
|
240
|
+
resolvedSharedDir = detectSharedDir();
|
|
241
|
+
if (resolvedSharedDir)
|
|
242
|
+
console.log(` Auto-detected shared settings: ${resolvedSharedDir}`);
|
|
243
|
+
}
|
|
213
244
|
const config = {
|
|
214
245
|
...existing,
|
|
215
|
-
sharedDir:
|
|
246
|
+
sharedDir: resolvedSharedDir,
|
|
216
247
|
storePath: storePath || existing.storePath || DEFAULT_STORE_PATH,
|
|
217
248
|
};
|
|
249
|
+
fs.mkdirSync(LOCAL_DIR, { recursive: true });
|
|
218
250
|
atomicWrite(LOCAL_CONFIG_PATH, config);
|
|
219
251
|
}
|
|
220
252
|
const DEFAULT_SETTINGS = {
|