@bobfrankston/mailx 1.0.12 → 1.0.14

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.
Files changed (172) hide show
  1. package/bin/mailx.js +52 -28
  2. package/client/app.js +113 -30
  3. package/client/components/folder-tree.js +84 -3
  4. package/client/components/message-list.js +164 -10
  5. package/client/components/message-viewer.js +130 -13
  6. package/client/compose/compose.html +4 -4
  7. package/client/compose/compose.js +53 -34
  8. package/client/index.html +50 -21
  9. package/client/lib/api-client.js +112 -31
  10. package/client/lib/mailxapi.js +123 -0
  11. package/client/package.json +1 -1
  12. package/client/styles/components.css +206 -16
  13. package/client/styles/layout.css +2 -1
  14. package/killmail.cmd +6 -0
  15. package/launch.ps1 +47 -5
  16. package/launcher/bin/mailx-app-linux +0 -0
  17. package/launcher/bin/mailx-app.exe +0 -0
  18. package/launcher/builder/build-config.json +11 -0
  19. package/launcher/builder/postinstall.js +81 -0
  20. package/package.json +2 -4
  21. package/packages/mailx-api/index.js +125 -29
  22. package/packages/mailx-core/index.d.ts +129 -0
  23. package/packages/mailx-core/index.js +323 -0
  24. package/packages/mailx-core/ipc.d.ts +13 -0
  25. package/packages/mailx-core/ipc.js +56 -0
  26. package/packages/mailx-core/package.json +18 -0
  27. package/packages/mailx-imap/index.d.ts +7 -1
  28. package/packages/mailx-imap/index.js +89 -14
  29. package/packages/mailx-server/index.js +42 -31
  30. package/packages/mailx-server/package.json +1 -2
  31. package/packages/mailx-settings/index.d.ts +1 -1
  32. package/packages/mailx-settings/index.js +21 -12
  33. package/packages/mailx-store/db.d.ts +6 -2
  34. package/packages/mailx-store/db.js +78 -16
  35. package/packages/mailx-store/file-store.d.ts +2 -8
  36. package/packages/mailx-store/file-store.js +7 -31
  37. package/packages/mailx-types/index.d.ts +3 -1
  38. package/.tswalk.json +0 -7396
  39. package/launcher/release.cmd +0 -4
  40. package/mailx.json +0 -9
  41. package/packages/mailx-api/node_modules/nodemailer/.ncurc.js +0 -9
  42. package/packages/mailx-api/node_modules/nodemailer/.prettierignore +0 -8
  43. package/packages/mailx-api/node_modules/nodemailer/.prettierrc +0 -12
  44. package/packages/mailx-api/node_modules/nodemailer/.prettierrc.js +0 -10
  45. package/packages/mailx-api/node_modules/nodemailer/.release-please-config.json +0 -9
  46. package/packages/mailx-api/node_modules/nodemailer/LICENSE +0 -16
  47. package/packages/mailx-api/node_modules/nodemailer/README.md +0 -86
  48. package/packages/mailx-api/node_modules/nodemailer/SECURITY.txt +0 -22
  49. package/packages/mailx-api/node_modules/nodemailer/eslint.config.js +0 -88
  50. package/packages/mailx-api/node_modules/nodemailer/lib/addressparser/index.js +0 -383
  51. package/packages/mailx-api/node_modules/nodemailer/lib/base64/index.js +0 -139
  52. package/packages/mailx-api/node_modules/nodemailer/lib/dkim/index.js +0 -253
  53. package/packages/mailx-api/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
  54. package/packages/mailx-api/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
  55. package/packages/mailx-api/node_modules/nodemailer/lib/dkim/sign.js +0 -117
  56. package/packages/mailx-api/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
  57. package/packages/mailx-api/node_modules/nodemailer/lib/fetch/index.js +0 -280
  58. package/packages/mailx-api/node_modules/nodemailer/lib/json-transport/index.js +0 -82
  59. package/packages/mailx-api/node_modules/nodemailer/lib/mail-composer/index.js +0 -629
  60. package/packages/mailx-api/node_modules/nodemailer/lib/mailer/index.js +0 -441
  61. package/packages/mailx-api/node_modules/nodemailer/lib/mailer/mail-message.js +0 -316
  62. package/packages/mailx-api/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
  63. package/packages/mailx-api/node_modules/nodemailer/lib/mime-funcs/mime-types.js +0 -2113
  64. package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/index.js +0 -1316
  65. package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/last-newline.js +0 -33
  66. package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/le-unix.js +0 -43
  67. package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/le-windows.js +0 -52
  68. package/packages/mailx-api/node_modules/nodemailer/lib/nodemailer.js +0 -157
  69. package/packages/mailx-api/node_modules/nodemailer/lib/punycode/index.js +0 -460
  70. package/packages/mailx-api/node_modules/nodemailer/lib/qp/index.js +0 -227
  71. package/packages/mailx-api/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
  72. package/packages/mailx-api/node_modules/nodemailer/lib/ses-transport/index.js +0 -234
  73. package/packages/mailx-api/node_modules/nodemailer/lib/shared/index.js +0 -754
  74. package/packages/mailx-api/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
  75. package/packages/mailx-api/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
  76. package/packages/mailx-api/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1870
  77. package/packages/mailx-api/node_modules/nodemailer/lib/smtp-pool/index.js +0 -652
  78. package/packages/mailx-api/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +0 -259
  79. package/packages/mailx-api/node_modules/nodemailer/lib/smtp-transport/index.js +0 -421
  80. package/packages/mailx-api/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
  81. package/packages/mailx-api/node_modules/nodemailer/lib/well-known/index.js +0 -47
  82. package/packages/mailx-api/node_modules/nodemailer/lib/well-known/services.json +0 -611
  83. package/packages/mailx-api/node_modules/nodemailer/lib/xoauth2/index.js +0 -427
  84. package/packages/mailx-api/node_modules/nodemailer/package.json +0 -47
  85. package/packages/mailx-imap/node_modules/nodemailer/.ncurc.js +0 -9
  86. package/packages/mailx-imap/node_modules/nodemailer/.prettierignore +0 -8
  87. package/packages/mailx-imap/node_modules/nodemailer/.prettierrc +0 -12
  88. package/packages/mailx-imap/node_modules/nodemailer/.prettierrc.js +0 -10
  89. package/packages/mailx-imap/node_modules/nodemailer/.release-please-config.json +0 -9
  90. package/packages/mailx-imap/node_modules/nodemailer/LICENSE +0 -16
  91. package/packages/mailx-imap/node_modules/nodemailer/README.md +0 -86
  92. package/packages/mailx-imap/node_modules/nodemailer/SECURITY.txt +0 -22
  93. package/packages/mailx-imap/node_modules/nodemailer/eslint.config.js +0 -88
  94. package/packages/mailx-imap/node_modules/nodemailer/lib/addressparser/index.js +0 -383
  95. package/packages/mailx-imap/node_modules/nodemailer/lib/base64/index.js +0 -139
  96. package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/index.js +0 -253
  97. package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
  98. package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
  99. package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/sign.js +0 -117
  100. package/packages/mailx-imap/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
  101. package/packages/mailx-imap/node_modules/nodemailer/lib/fetch/index.js +0 -280
  102. package/packages/mailx-imap/node_modules/nodemailer/lib/json-transport/index.js +0 -82
  103. package/packages/mailx-imap/node_modules/nodemailer/lib/mail-composer/index.js +0 -629
  104. package/packages/mailx-imap/node_modules/nodemailer/lib/mailer/index.js +0 -441
  105. package/packages/mailx-imap/node_modules/nodemailer/lib/mailer/mail-message.js +0 -316
  106. package/packages/mailx-imap/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
  107. package/packages/mailx-imap/node_modules/nodemailer/lib/mime-funcs/mime-types.js +0 -2113
  108. package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/index.js +0 -1316
  109. package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/last-newline.js +0 -33
  110. package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/le-unix.js +0 -43
  111. package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/le-windows.js +0 -52
  112. package/packages/mailx-imap/node_modules/nodemailer/lib/nodemailer.js +0 -157
  113. package/packages/mailx-imap/node_modules/nodemailer/lib/punycode/index.js +0 -460
  114. package/packages/mailx-imap/node_modules/nodemailer/lib/qp/index.js +0 -227
  115. package/packages/mailx-imap/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
  116. package/packages/mailx-imap/node_modules/nodemailer/lib/ses-transport/index.js +0 -234
  117. package/packages/mailx-imap/node_modules/nodemailer/lib/shared/index.js +0 -754
  118. package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
  119. package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
  120. package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1870
  121. package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-pool/index.js +0 -652
  122. package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +0 -259
  123. package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-transport/index.js +0 -421
  124. package/packages/mailx-imap/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
  125. package/packages/mailx-imap/node_modules/nodemailer/lib/well-known/index.js +0 -47
  126. package/packages/mailx-imap/node_modules/nodemailer/lib/well-known/services.json +0 -611
  127. package/packages/mailx-imap/node_modules/nodemailer/lib/xoauth2/index.js +0 -427
  128. package/packages/mailx-imap/node_modules/nodemailer/package.json +0 -47
  129. package/packages/mailx-send/node_modules/nodemailer/.ncurc.js +0 -9
  130. package/packages/mailx-send/node_modules/nodemailer/.prettierignore +0 -8
  131. package/packages/mailx-send/node_modules/nodemailer/.prettierrc +0 -12
  132. package/packages/mailx-send/node_modules/nodemailer/.prettierrc.js +0 -10
  133. package/packages/mailx-send/node_modules/nodemailer/.release-please-config.json +0 -9
  134. package/packages/mailx-send/node_modules/nodemailer/LICENSE +0 -16
  135. package/packages/mailx-send/node_modules/nodemailer/README.md +0 -86
  136. package/packages/mailx-send/node_modules/nodemailer/SECURITY.txt +0 -22
  137. package/packages/mailx-send/node_modules/nodemailer/eslint.config.js +0 -88
  138. package/packages/mailx-send/node_modules/nodemailer/lib/addressparser/index.js +0 -383
  139. package/packages/mailx-send/node_modules/nodemailer/lib/base64/index.js +0 -139
  140. package/packages/mailx-send/node_modules/nodemailer/lib/dkim/index.js +0 -253
  141. package/packages/mailx-send/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
  142. package/packages/mailx-send/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
  143. package/packages/mailx-send/node_modules/nodemailer/lib/dkim/sign.js +0 -117
  144. package/packages/mailx-send/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
  145. package/packages/mailx-send/node_modules/nodemailer/lib/fetch/index.js +0 -280
  146. package/packages/mailx-send/node_modules/nodemailer/lib/json-transport/index.js +0 -82
  147. package/packages/mailx-send/node_modules/nodemailer/lib/mail-composer/index.js +0 -629
  148. package/packages/mailx-send/node_modules/nodemailer/lib/mailer/index.js +0 -441
  149. package/packages/mailx-send/node_modules/nodemailer/lib/mailer/mail-message.js +0 -316
  150. package/packages/mailx-send/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
  151. package/packages/mailx-send/node_modules/nodemailer/lib/mime-funcs/mime-types.js +0 -2113
  152. package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/index.js +0 -1316
  153. package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/last-newline.js +0 -33
  154. package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/le-unix.js +0 -43
  155. package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/le-windows.js +0 -52
  156. package/packages/mailx-send/node_modules/nodemailer/lib/nodemailer.js +0 -157
  157. package/packages/mailx-send/node_modules/nodemailer/lib/punycode/index.js +0 -460
  158. package/packages/mailx-send/node_modules/nodemailer/lib/qp/index.js +0 -227
  159. package/packages/mailx-send/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
  160. package/packages/mailx-send/node_modules/nodemailer/lib/ses-transport/index.js +0 -234
  161. package/packages/mailx-send/node_modules/nodemailer/lib/shared/index.js +0 -754
  162. package/packages/mailx-send/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
  163. package/packages/mailx-send/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
  164. package/packages/mailx-send/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1870
  165. package/packages/mailx-send/node_modules/nodemailer/lib/smtp-pool/index.js +0 -652
  166. package/packages/mailx-send/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +0 -259
  167. package/packages/mailx-send/node_modules/nodemailer/lib/smtp-transport/index.js +0 -421
  168. package/packages/mailx-send/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
  169. package/packages/mailx-send/node_modules/nodemailer/lib/well-known/index.js +0 -47
  170. package/packages/mailx-send/node_modules/nodemailer/lib/well-known/services.json +0 -611
  171. package/packages/mailx-send/node_modules/nodemailer/lib/xoauth2/index.js +0 -427
  172. package/packages/mailx-send/node_modules/nodemailer/package.json +0 -47
@@ -3,7 +3,7 @@
3
3
  * Express Router with all REST endpoints for the mailx client.
4
4
  */
5
5
  import { Router } from "express";
6
- import { loadSettings, saveSettings, loadAllowlist, saveAllowlist } from "@bobfrankston/mailx-settings";
6
+ import { loadSettings, saveSettings, loadAllowlist, saveAllowlist, getStorePath } from "@bobfrankston/mailx-settings";
7
7
  import { simpleParser } from "mailparser";
8
8
  /** Sanitize HTML email body — strip remote content (images, CSS, scripts, event handlers) */
9
9
  function sanitizeHtml(html) {
@@ -42,7 +42,13 @@ export function createApiRouter(db, imapManager) {
42
42
  // ── Accounts ──
43
43
  router.get("/accounts", (req, res) => {
44
44
  const accounts = db.getAccounts();
45
- res.json(accounts);
45
+ const settings = loadSettings();
46
+ // Merge settings flags
47
+ const enriched = accounts.map(a => {
48
+ const cfg = settings.accounts.find(s => s.id === a.id);
49
+ return { ...a, label: cfg?.label, defaultSend: cfg?.defaultSend || false };
50
+ });
51
+ res.json(enriched);
46
52
  });
47
53
  // ── Folders ──
48
54
  router.get("/folders/:accountId", (req, res) => {
@@ -54,22 +60,8 @@ export function createApiRouter(db, imapManager) {
54
60
  router.get("/messages/unified/inbox", (req, res) => {
55
61
  const page = Number(req.query.page) || 1;
56
62
  const pageSize = Number(req.query.pageSize) || 50;
57
- const accounts = db.getAccounts();
58
- let allItems = [];
59
- for (const account of accounts) {
60
- const folders = db.getFolders(account.id);
61
- const inbox = folders.find(f => f.specialUse === "inbox");
62
- if (!inbox)
63
- continue;
64
- const result = db.getMessages({ accountId: account.id, folderId: inbox.id, page: 1, pageSize: 10000 });
65
- allItems.push(...result.items);
66
- }
67
- // Sort by date descending
68
- allItems.sort((a, b) => b.date - a.date);
69
- const total = allItems.length;
70
- const offset = (page - 1) * pageSize;
71
- const items = allItems.slice(offset, offset + pageSize);
72
- res.json({ items, total, page, pageSize });
63
+ const result = db.getUnifiedInbox(page, pageSize);
64
+ res.json(result);
73
65
  });
74
66
  // Per-folder messages (after unified to avoid route conflict)
75
67
  router.get("/messages/:accountId/:folderId", (req, res) => {
@@ -88,7 +80,8 @@ export function createApiRouter(db, imapManager) {
88
80
  try {
89
81
  const { accountId, uid } = req.params;
90
82
  let allowRemote = req.query.allowRemote === "true";
91
- const envelope = db.getMessageByUid(accountId, Number(uid));
83
+ const folderId = req.query.folderId ? Number(req.query.folderId) : undefined;
84
+ const envelope = db.getMessageByUid(accountId, Number(uid), folderId);
92
85
  if (!envelope)
93
86
  return res.status(404).json({ error: "Message not found" });
94
87
  // Load body from store or fetch on demand from IMAP
@@ -132,12 +125,42 @@ export function createApiRouter(db, imapManager) {
132
125
  hasRemoteContent = result.hasRemoteContent;
133
126
  }
134
127
  }
128
+ // Build .eml file path
129
+ const storePath = getStorePath();
130
+ const emlPath = `${storePath}/${accountId}/${envelope.folderId}/${envelope.uid}.eml`;
131
+ // Extract useful headers (values may be strings or structured objects)
132
+ let deliveredTo = "";
133
+ let returnPath = "";
134
+ let listUnsubscribe = "";
135
+ if (raw) {
136
+ const parsed2 = await simpleParser(raw);
137
+ const hdr = (key) => {
138
+ const v = parsed2.headers.get(key);
139
+ if (!v)
140
+ return "";
141
+ if (typeof v === "string")
142
+ return v;
143
+ if (typeof v === "object" && "text" in v)
144
+ return v.text || "";
145
+ if (typeof v === "object" && "value" in v)
146
+ return String(v.value);
147
+ return String(v);
148
+ };
149
+ deliveredTo = hdr("delivered-to");
150
+ returnPath = hdr("return-path").replace(/[<>]/g, "");
151
+ listUnsubscribe = hdr("list-unsubscribe");
152
+ }
135
153
  const message = {
136
154
  ...envelope,
137
155
  bodyHtml,
138
156
  bodyText,
139
157
  hasRemoteContent,
140
- attachments
158
+ remoteAllowed: allowRemote,
159
+ attachments,
160
+ emlPath,
161
+ deliveredTo,
162
+ returnPath,
163
+ listUnsubscribe,
141
164
  };
142
165
  res.json(message);
143
166
  }
@@ -174,14 +197,57 @@ export function createApiRouter(db, imapManager) {
174
197
  res.json({ ok: true });
175
198
  });
176
199
  // ── Search ──
177
- router.get("/search", (req, res) => {
200
+ router.get("/search", async (req, res) => {
178
201
  const q = req.query.q || "";
179
202
  const page = Number(req.query.page) || 1;
180
203
  const pageSize = Number(req.query.pageSize) || 50;
204
+ const scope = req.query.scope || "all";
205
+ const accountId = req.query.accountId || "";
206
+ const folderId = Number(req.query.folderId) || 0;
181
207
  if (!q.trim())
182
208
  return res.json({ items: [], total: 0, page, pageSize });
183
- const result = db.searchMessages(q, page, pageSize);
184
- res.json(result);
209
+ try {
210
+ if (scope === "server" && accountId) {
211
+ // IMAP server search
212
+ const folders = db.getFolders(accountId);
213
+ const folder = folderId ? folders.find(f => f.id === folderId) : folders.find(f => f.specialUse === "inbox");
214
+ if (!folder)
215
+ return res.json({ items: [], total: 0, page, pageSize });
216
+ const criteria = {};
217
+ // Parse qualifiers
218
+ const fromMatch = q.match(/from:(\S+)/i);
219
+ const toMatch = q.match(/to:(\S+)/i);
220
+ const subjectMatch = q.match(/subject:(.+?)(?:\s+\w+:|$)/i);
221
+ const bodyText = q.replace(/(?:from|to|subject):\S+/gi, "").trim();
222
+ if (fromMatch)
223
+ criteria.from = fromMatch[1];
224
+ if (toMatch)
225
+ criteria.to = toMatch[1];
226
+ if (subjectMatch)
227
+ criteria.subject = subjectMatch[1].trim();
228
+ if (bodyText)
229
+ criteria.body = bodyText;
230
+ const uids = await imapManager.searchOnServer(accountId, folder.path, criteria);
231
+ // Fetch envelopes for matching UIDs from local DB
232
+ const items = uids.slice((page - 1) * pageSize, page * pageSize)
233
+ .map(uid => db.getMessageByUid(accountId, uid, folderId))
234
+ .filter(Boolean);
235
+ res.json({ items, total: uids.length, page, pageSize });
236
+ }
237
+ else if (scope === "current" && accountId && folderId) {
238
+ // Search within current folder only
239
+ const result = db.searchMessages(q, page, pageSize, accountId, folderId);
240
+ res.json(result);
241
+ }
242
+ else {
243
+ // All folders (default)
244
+ const result = db.searchMessages(q, page, pageSize);
245
+ res.json(result);
246
+ }
247
+ }
248
+ catch (e) {
249
+ res.status(500).json({ error: e.message });
250
+ }
185
251
  });
186
252
  router.post("/search/rebuild", (req, res) => {
187
253
  const count = db.rebuildSearchIndex();
@@ -237,12 +303,16 @@ export function createApiRouter(db, imapManager) {
237
303
  const account = settings.accounts.find(a => a.id === msg.from);
238
304
  if (!account)
239
305
  return res.status(400).json({ error: `Unknown account: ${msg.from}` });
306
+ // Use custom From address if provided, otherwise account default
307
+ const fromHeader = msg.fromAddress || `${account.name} <${account.email}>`;
240
308
  // Build RFC 822 message
241
- const to = msg.to.map(a => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
242
- const cc = msg.cc?.map(a => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
243
- const bcc = msg.bcc?.map(a => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
309
+ const to = msg.to.map((a) => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
310
+ const cc = msg.cc?.map((a) => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
311
+ const bcc = msg.bcc?.map((a) => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
312
+ const body = msg.bodyHtml || msg.bodyText || "";
313
+ const bodyBase64 = Buffer.from(body, "utf-8").toString("base64").replace(/(.{76})/g, "$1\r\n");
244
314
  const headers = [
245
- `From: ${account.name} <${account.email}>`,
315
+ `From: ${fromHeader}`,
246
316
  `To: ${to}`,
247
317
  cc ? `Cc: ${cc}` : null,
248
318
  bcc ? `Bcc: ${bcc}` : null,
@@ -252,11 +322,12 @@ export function createApiRouter(db, imapManager) {
252
322
  msg.references?.length ? `References: ${msg.references.join(" ")}` : null,
253
323
  `MIME-Version: 1.0`,
254
324
  `Content-Type: text/html; charset=UTF-8`,
325
+ `Content-Transfer-Encoding: base64`,
255
326
  ].filter(h => h !== null).join("\r\n");
256
- const rawMessage = `${headers}\r\n\r\n${msg.bodyHtml || msg.bodyText || ""}`;
327
+ const rawMessage = `${headers}\r\n\r\n${bodyBase64}`;
257
328
  // Local-first: save to sync queue, worker will APPEND to Outbox and send
258
329
  imapManager.queueOutgoingLocal(account.id, rawMessage);
259
- console.log(` Queued locally: ${msg.subject} via ${account.id}`);
330
+ console.log(` Queued locally: ${msg.subject} via ${account.id} from ${fromHeader}`);
260
331
  // Record recipient addresses for autocomplete
261
332
  for (const addr of msg.to)
262
333
  db.recordSentAddress(addr.name, addr.address);
@@ -288,6 +359,31 @@ export function createApiRouter(db, imapManager) {
288
359
  res.status(500).json({ error: e.message });
289
360
  }
290
361
  });
362
+ // ── Move message to another folder ──
363
+ router.post("/message/:accountId/:uid/move", async (req, res) => {
364
+ try {
365
+ const { accountId, uid } = req.params;
366
+ const { targetFolderId, targetAccountId } = req.body;
367
+ if (targetFolderId == null)
368
+ return res.status(400).json({ error: "targetFolderId required" });
369
+ const envelope = db.getMessageByUid(accountId, Number(uid));
370
+ if (!envelope)
371
+ return res.status(404).json({ error: "Message not found" });
372
+ if (targetAccountId && targetAccountId !== accountId) {
373
+ // Cross-account move via iflow
374
+ await imapManager.moveMessageCrossAccount(accountId, envelope.uid, envelope.folderId, targetAccountId, targetFolderId);
375
+ }
376
+ else {
377
+ // Same-account move
378
+ await imapManager.moveMessage(accountId, envelope.uid, envelope.folderId, targetFolderId);
379
+ }
380
+ res.json({ ok: true });
381
+ }
382
+ catch (e) {
383
+ console.error(` Move error: ${e.message}`);
384
+ res.status(500).json({ error: e.message });
385
+ }
386
+ });
291
387
  // ── Undelete (move from Trash back to original folder) ──
292
388
  router.post("/message/:accountId/:uid/undelete", async (req, res) => {
293
389
  try {
@@ -0,0 +1,129 @@
1
+ /**
2
+ * @bobfrankston/mailx-core
3
+ * Core mail functions — callable directly via IPC or wrapped by Express.
4
+ * No HTTP, no Express, no WebSocket. Just plain async functions.
5
+ */
6
+ import { MailxDB } from "@bobfrankston/mailx-store";
7
+ import { ImapManager } from "@bobfrankston/mailx-imap";
8
+ import type { ComposeMessage } from "@bobfrankston/mailx-types";
9
+ declare let db: MailxDB;
10
+ declare let imapManager: ImapManager;
11
+ export declare function onEvent(handler: (event: any) => void): void;
12
+ export declare function initialize(): Promise<void>;
13
+ export declare function shutdown(): Promise<void>;
14
+ export declare function getAccounts(): {
15
+ id: string;
16
+ name: string;
17
+ email: string;
18
+ lastSync: number;
19
+ }[];
20
+ export declare function getFolders(params: {
21
+ accountId: string;
22
+ }): import("@bobfrankston/mailx-types").Folder[];
23
+ export declare function getMessages(params: {
24
+ accountId: string;
25
+ folderId: number;
26
+ page?: number;
27
+ pageSize?: number;
28
+ sort?: string;
29
+ sortDir?: string;
30
+ search?: string;
31
+ }): import("@bobfrankston/mailx-types").PagedResult<import("@bobfrankston/mailx-types").MessageEnvelope>;
32
+ export declare function getUnifiedInbox(params: {
33
+ page?: number;
34
+ pageSize?: number;
35
+ }): import("@bobfrankston/mailx-types").PagedResult<import("@bobfrankston/mailx-types").MessageEnvelope>;
36
+ export declare function getMessage(params: {
37
+ accountId: string;
38
+ uid: number;
39
+ allowRemote?: boolean;
40
+ folderId?: number;
41
+ }): Promise<{
42
+ bodyHtml: string;
43
+ bodyText: string;
44
+ hasRemoteContent: boolean;
45
+ remoteAllowed: boolean;
46
+ attachments: any[];
47
+ deliveredTo: string;
48
+ returnPath: string;
49
+ listUnsubscribe: string;
50
+ emlPath: string;
51
+ id: number;
52
+ accountId: string;
53
+ folderId: number;
54
+ uid: number;
55
+ messageId: string;
56
+ inReplyTo: string;
57
+ references: string[];
58
+ date: number;
59
+ subject: string;
60
+ from: import("@bobfrankston/mailx-types").EmailAddress;
61
+ to: import("@bobfrankston/mailx-types").EmailAddress[];
62
+ cc: import("@bobfrankston/mailx-types").EmailAddress[];
63
+ flags: string[];
64
+ size: number;
65
+ hasAttachments: boolean;
66
+ preview: string;
67
+ }>;
68
+ export declare function updateFlags(params: {
69
+ accountId: string;
70
+ uid: number;
71
+ flags: string[];
72
+ }): Promise<void>;
73
+ export declare function deleteMessage(params: {
74
+ accountId: string;
75
+ uid: number;
76
+ }): Promise<void>;
77
+ export declare function undeleteMessage(params: {
78
+ accountId: string;
79
+ uid: number;
80
+ folderId: number;
81
+ }): Promise<void>;
82
+ export declare function sendMessage(params: ComposeMessage): Promise<void>;
83
+ export declare function saveDraft(params: {
84
+ accountId: string;
85
+ subject: string;
86
+ bodyHtml: string;
87
+ bodyText: string;
88
+ to: string;
89
+ cc: string;
90
+ previousDraftUid?: number;
91
+ }): Promise<number>;
92
+ export declare function deleteDraft(params: {
93
+ accountId: string;
94
+ draftUid: number;
95
+ }): Promise<void>;
96
+ export declare function searchMessages(params: {
97
+ query: string;
98
+ page?: number;
99
+ pageSize?: number;
100
+ }): import("@bobfrankston/mailx-types").PagedResult<import("@bobfrankston/mailx-types").MessageEnvelope>;
101
+ export declare function searchContacts(params: {
102
+ query: string;
103
+ }): {
104
+ name: string;
105
+ email: string;
106
+ source: string;
107
+ useCount: number;
108
+ }[];
109
+ export declare function syncAll(): Promise<void>;
110
+ export declare function getSyncPending(): {
111
+ pending: number;
112
+ };
113
+ export declare function allowRemoteContent(params: {
114
+ type: string;
115
+ value: string;
116
+ }): void;
117
+ export declare function getSettings(): import("@bobfrankston/mailx-types").MailxSettings;
118
+ export declare function saveSettingsData(data: any): void;
119
+ export declare function rebuildSearchIndex(): number;
120
+ export declare function seedContacts(): number;
121
+ export declare function syncGoogleContacts(): Promise<void>;
122
+ export declare function getVersion(): {
123
+ server: string;
124
+ client: string;
125
+ };
126
+ /** Dispatch an action by name — used by IPC and Express wrapper */
127
+ export declare function dispatch(action: string, params?: any): Promise<any>;
128
+ export { db, imapManager };
129
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,323 @@
1
+ /**
2
+ * @bobfrankston/mailx-core
3
+ * Core mail functions — callable directly via IPC or wrapped by Express.
4
+ * No HTTP, no Express, no WebSocket. Just plain async functions.
5
+ */
6
+ import { MailxDB } from "@bobfrankston/mailx-store";
7
+ import { ImapManager } from "@bobfrankston/mailx-imap";
8
+ import { loadSettings, saveSettings, loadAllowlist, saveAllowlist, getConfigDir, getStorePath, initLocalConfig } from "@bobfrankston/mailx-settings";
9
+ import { simpleParser } from "mailparser";
10
+ // ── Initialization ──
11
+ let db;
12
+ let imapManager;
13
+ const eventHandlers = [];
14
+ export function onEvent(handler) {
15
+ eventHandlers.push(handler);
16
+ }
17
+ function emit(event) {
18
+ for (const h of eventHandlers) {
19
+ try {
20
+ h(event);
21
+ }
22
+ catch { /* ignore handler errors */ }
23
+ }
24
+ }
25
+ export async function initialize() {
26
+ initLocalConfig();
27
+ const dbDir = getConfigDir();
28
+ db = new MailxDB(dbDir);
29
+ imapManager = new ImapManager(db);
30
+ // Seed contacts
31
+ const seeded = db.seedContactsFromMessages();
32
+ if (seeded > 0)
33
+ console.log(` Seeded ${seeded} contacts`);
34
+ // Search index — only if empty
35
+ let ftsCount = 0;
36
+ try {
37
+ ftsCount = db.db.prepare("SELECT COUNT(*) as cnt FROM messages_fts").get()?.cnt || 0;
38
+ }
39
+ catch { /* */ }
40
+ if (ftsCount === 0) {
41
+ const indexed = db.rebuildSearchIndex();
42
+ if (indexed > 0)
43
+ console.log(` Search index: ${indexed} messages`);
44
+ }
45
+ // Add accounts
46
+ const settings = loadSettings();
47
+ for (const account of settings.accounts) {
48
+ if (!account.enabled)
49
+ continue;
50
+ try {
51
+ await imapManager.addAccount(account);
52
+ console.log(` Account added: ${account.name} (${account.id})`);
53
+ }
54
+ catch (e) {
55
+ console.error(` Failed to add account ${account.id}: ${e.message}`);
56
+ }
57
+ }
58
+ // Wire events to push notifications
59
+ imapManager.on("syncProgress", (accountId, phase, progress) => {
60
+ emit({ type: "syncProgress", accountId, phase, progress });
61
+ });
62
+ imapManager.on("folderCountsChanged", (accountId, counts) => {
63
+ emit({ type: "folderCountsChanged", accountId, counts });
64
+ });
65
+ // Initial sync + IDLE
66
+ if (settings.accounts.some(a => a.enabled)) {
67
+ console.log(" Starting initial sync...");
68
+ imapManager.syncAll().then(async () => {
69
+ console.log(" Initial sync complete");
70
+ await imapManager.startWatching();
71
+ imapManager.syncAllContacts().catch(e => console.error(` Google Contacts sync error: ${e.message}`));
72
+ }).catch(e => {
73
+ console.error(` Initial sync error: ${e.message}`);
74
+ });
75
+ }
76
+ imapManager.startPeriodicSync(settings.sync.intervalMinutes);
77
+ imapManager.startOutboxWorker();
78
+ }
79
+ export async function shutdown() {
80
+ imapManager?.stopPeriodicSync();
81
+ imapManager?.stopOutboxWorker();
82
+ await imapManager?.shutdown();
83
+ db?.close();
84
+ }
85
+ // ── HTML Sanitization ──
86
+ function sanitizeHtml(html) {
87
+ let hasRemoteContent = false;
88
+ let clean = html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "");
89
+ clean = clean.replace(/\s+on\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, "");
90
+ clean = clean.replace(/<img\b([^>]*)\bsrc\s*=\s*("[^"]*"|'[^']*')/gi, (match, before, src) => {
91
+ const url = src.slice(1, -1);
92
+ if (url.startsWith("data:") || url.startsWith("cid:"))
93
+ return match;
94
+ hasRemoteContent = true;
95
+ return `<img${before}src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Crect fill='%23888' width='20' height='20' rx='3'/%3E%3Ctext x='10' y='14' text-anchor='middle' fill='white' font-size='12'%3E⊘%3C/text%3E%3C/svg%3E" data-blocked-src=${src} title="Remote image blocked"`;
96
+ });
97
+ clean = clean.replace(/<link\b[^>]*rel\s*=\s*["']stylesheet["'][^>]*>/gi, (match) => {
98
+ hasRemoteContent = true;
99
+ return `<!-- blocked: ${match.replace(/--/g, "")} -->`;
100
+ });
101
+ clean = clean.replace(/url\s*\(\s*(['"]?)(https?:\/\/[^)]+)\1\s*\)/gi, (_match, _q, url) => {
102
+ hasRemoteContent = true;
103
+ return `url("") /* blocked: ${url} */`;
104
+ });
105
+ clean = clean.replace(/<\/?form\b[^>]*>/gi, "");
106
+ clean = clean.replace(/<iframe\b[^>]*>[\s\S]*?<\/iframe>/gi, "");
107
+ return { html: clean, hasRemoteContent };
108
+ }
109
+ // ── API Functions ──
110
+ export function getAccounts() {
111
+ return db.getAccounts();
112
+ }
113
+ export function getFolders(params) {
114
+ return db.getFolders(params.accountId);
115
+ }
116
+ export function getMessages(params) {
117
+ return db.getMessages({
118
+ accountId: params.accountId,
119
+ folderId: params.folderId,
120
+ page: params.page || 1,
121
+ pageSize: params.pageSize || 50,
122
+ sort: params.sort || "date",
123
+ sortDir: params.sortDir || "desc",
124
+ search: params.search,
125
+ });
126
+ }
127
+ export function getUnifiedInbox(params) {
128
+ return db.getUnifiedInbox(params.page, params.pageSize);
129
+ }
130
+ export async function getMessage(params) {
131
+ const { accountId, uid } = params;
132
+ let allowRemote = params.allowRemote || false;
133
+ const envelope = db.getMessageByUid(accountId, uid, params.folderId);
134
+ if (!envelope)
135
+ throw new Error("Message not found");
136
+ let bodyHtml = "";
137
+ let bodyText = "";
138
+ let hasRemoteContent = false;
139
+ let attachments = [];
140
+ let deliveredTo = "";
141
+ let returnPath = "";
142
+ let listUnsubscribe = "";
143
+ const raw = await imapManager.fetchMessageBody(accountId, envelope.folderId, envelope.uid);
144
+ if (raw) {
145
+ const parsed = await simpleParser(raw);
146
+ bodyHtml = parsed.html || "";
147
+ bodyText = parsed.text || "";
148
+ attachments = (parsed.attachments || []).map((a, i) => ({
149
+ id: i,
150
+ filename: a.filename || `attachment-${i}`,
151
+ mimeType: a.contentType || "application/octet-stream",
152
+ size: a.size || 0,
153
+ contentId: a.contentId || "",
154
+ }));
155
+ // Extract useful headers for the UI
156
+ const hdr = (key) => {
157
+ const v = parsed.headers.get(key);
158
+ if (!v)
159
+ return "";
160
+ if (typeof v === "string")
161
+ return v;
162
+ if (typeof v === "object" && "text" in v)
163
+ return v.text || "";
164
+ if (typeof v === "object" && "value" in v)
165
+ return String(v.value);
166
+ return String(v);
167
+ };
168
+ deliveredTo = hdr("delivered-to");
169
+ returnPath = hdr("return-path").replace(/[<>]/g, "");
170
+ listUnsubscribe = hdr("list-unsubscribe");
171
+ }
172
+ if (bodyHtml && !allowRemote) {
173
+ const allowList = loadAllowlist();
174
+ const senderAddr = envelope.from?.address || "";
175
+ const senderDomain = senderAddr.split("@")[1] || "";
176
+ const toAddrs = (envelope.to || []).map((a) => a.address);
177
+ const isAllowed = allowList.senders.includes(senderAddr) ||
178
+ allowList.domains.includes(senderDomain) ||
179
+ toAddrs.some((a) => allowList.recipients?.includes(a));
180
+ if (isAllowed) {
181
+ allowRemote = true;
182
+ }
183
+ else {
184
+ const result = sanitizeHtml(bodyHtml);
185
+ bodyHtml = result.html;
186
+ hasRemoteContent = result.hasRemoteContent;
187
+ }
188
+ }
189
+ // Build .eml file path for "View Source"
190
+ const storePath = getStorePath();
191
+ const emlPath = `${storePath}/${accountId}/${envelope.folderId}/${envelope.uid}.eml`;
192
+ return { ...envelope, bodyHtml, bodyText, hasRemoteContent, remoteAllowed: allowRemote, attachments, deliveredTo, returnPath, listUnsubscribe, emlPath };
193
+ }
194
+ export async function updateFlags(params) {
195
+ const envelope = db.getMessageByUid(params.accountId, params.uid);
196
+ await imapManager.updateFlagsLocal(params.accountId, params.uid, envelope?.folderId || 0, params.flags);
197
+ }
198
+ export async function deleteMessage(params) {
199
+ const envelope = db.getMessageByUid(params.accountId, params.uid);
200
+ if (!envelope)
201
+ throw new Error("Message not found");
202
+ await imapManager.trashMessage(params.accountId, envelope.folderId, envelope.uid);
203
+ }
204
+ export async function undeleteMessage(params) {
205
+ await imapManager.undeleteMessage(params.accountId, params.uid, params.folderId);
206
+ }
207
+ export async function sendMessage(params) {
208
+ const settings = loadSettings();
209
+ const account = settings.accounts.find(a => a.id === params.from);
210
+ if (!account)
211
+ throw new Error(`Unknown account: ${params.from}`);
212
+ const to = params.to.map(a => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
213
+ const cc = params.cc?.map(a => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
214
+ const bcc = params.bcc?.map(a => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
215
+ const headers = [
216
+ `From: ${account.name} <${account.email}>`,
217
+ `To: ${to}`,
218
+ cc ? `Cc: ${cc}` : null,
219
+ bcc ? `Bcc: ${bcc}` : null,
220
+ `Subject: ${params.subject}`,
221
+ `Date: ${new Date().toUTCString()}`,
222
+ params.inReplyTo ? `In-Reply-To: ${params.inReplyTo}` : null,
223
+ params.references?.length ? `References: ${params.references.join(" ")}` : null,
224
+ `MIME-Version: 1.0`,
225
+ `Content-Type: text/html; charset=UTF-8`,
226
+ ].filter(h => h !== null).join("\r\n");
227
+ const rawMessage = `${headers}\r\n\r\n${params.bodyHtml || params.bodyText || ""}`;
228
+ imapManager.queueOutgoingLocal(account.id, rawMessage);
229
+ for (const addr of params.to)
230
+ db.recordSentAddress(addr.name, addr.address);
231
+ if (params.cc)
232
+ for (const addr of params.cc)
233
+ db.recordSentAddress(addr.name, addr.address);
234
+ if (params.bcc)
235
+ for (const addr of params.bcc)
236
+ db.recordSentAddress(addr.name, addr.address);
237
+ }
238
+ export async function saveDraft(params) {
239
+ const settings = loadSettings();
240
+ const account = settings.accounts.find(a => a.id === params.accountId);
241
+ if (!account)
242
+ throw new Error(`Unknown account: ${params.accountId}`);
243
+ const headers = [
244
+ `From: ${account.name} <${account.email}>`,
245
+ params.to ? `To: ${params.to}` : null,
246
+ params.cc ? `Cc: ${params.cc}` : null,
247
+ `Subject: ${params.subject || "(no subject)"}`,
248
+ `Date: ${new Date().toUTCString()}`,
249
+ `MIME-Version: 1.0`,
250
+ `Content-Type: text/html; charset=UTF-8`,
251
+ ].filter(h => h !== null).join("\r\n");
252
+ const raw = `${headers}\r\n\r\n${params.bodyHtml || params.bodyText || ""}`;
253
+ return await imapManager.saveDraft(params.accountId, raw, params.previousDraftUid);
254
+ }
255
+ export async function deleteDraft(params) {
256
+ await imapManager.deleteDraft(params.accountId, params.draftUid);
257
+ }
258
+ export function searchMessages(params) {
259
+ if (!params.query.trim())
260
+ return { items: [], total: 0, page: 1, pageSize: 50 };
261
+ return db.searchMessages(params.query, params.page || 1, params.pageSize || 50);
262
+ }
263
+ export function searchContacts(params) {
264
+ if (params.query.length < 1)
265
+ return [];
266
+ return db.searchContacts(params.query);
267
+ }
268
+ export async function syncAll() {
269
+ await imapManager.syncAll();
270
+ }
271
+ export function getSyncPending() {
272
+ return { pending: db.getTotalPendingSyncCount() };
273
+ }
274
+ export function allowRemoteContent(params) {
275
+ const list = loadAllowlist();
276
+ if (params.type === "sender" && !list.senders.includes(params.value))
277
+ list.senders.push(params.value);
278
+ else if (params.type === "domain" && !list.domains.includes(params.value))
279
+ list.domains.push(params.value);
280
+ else if (params.type === "recipient") {
281
+ if (!list.recipients)
282
+ list.recipients = [];
283
+ if (!list.recipients.includes(params.value))
284
+ list.recipients.push(params.value);
285
+ }
286
+ saveAllowlist(list);
287
+ }
288
+ export function getSettings() {
289
+ return loadSettings();
290
+ }
291
+ export function saveSettingsData(data) {
292
+ saveSettings(data);
293
+ }
294
+ export function rebuildSearchIndex() {
295
+ return db.rebuildSearchIndex();
296
+ }
297
+ export function seedContacts() {
298
+ return db.seedContactsFromMessages();
299
+ }
300
+ export async function syncGoogleContacts() {
301
+ await imapManager.syncAllContacts();
302
+ }
303
+ export function getVersion() {
304
+ return { server: "1.0.0", client: "1.0.0" }; // Updated by build
305
+ }
306
+ // ── Action dispatcher for IPC ──
307
+ const actions = {
308
+ getAccounts, getFolders, getMessages, getUnifiedInbox, getMessage,
309
+ updateFlags, deleteMessage, undeleteMessage, sendMessage,
310
+ saveDraft, deleteDraft, searchMessages, searchContacts,
311
+ syncAll, getSyncPending, allowRemoteContent,
312
+ getSettings, saveSettingsData, rebuildSearchIndex,
313
+ seedContacts, syncGoogleContacts, getVersion,
314
+ };
315
+ /** Dispatch an action by name — used by IPC and Express wrapper */
316
+ export async function dispatch(action, params = {}) {
317
+ const fn = actions[action];
318
+ if (!fn)
319
+ throw new Error(`Unknown action: ${action}`);
320
+ return await fn(params);
321
+ }
322
+ export { db, imapManager };
323
+ //# sourceMappingURL=index.js.map