@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.
- package/bin/mailx.js +52 -28
- package/client/app.js +113 -30
- package/client/components/folder-tree.js +84 -3
- package/client/components/message-list.js +164 -10
- package/client/components/message-viewer.js +130 -13
- package/client/compose/compose.html +4 -4
- package/client/compose/compose.js +53 -34
- package/client/index.html +50 -21
- package/client/lib/api-client.js +112 -31
- package/client/lib/mailxapi.js +123 -0
- package/client/package.json +1 -1
- package/client/styles/components.css +206 -16
- package/client/styles/layout.css +2 -1
- package/killmail.cmd +6 -0
- package/launch.ps1 +47 -5
- package/launcher/bin/mailx-app-linux +0 -0
- package/launcher/bin/mailx-app.exe +0 -0
- package/launcher/builder/build-config.json +11 -0
- package/launcher/builder/postinstall.js +81 -0
- package/package.json +2 -4
- package/packages/mailx-api/index.js +125 -29
- package/packages/mailx-core/index.d.ts +129 -0
- package/packages/mailx-core/index.js +323 -0
- package/packages/mailx-core/ipc.d.ts +13 -0
- package/packages/mailx-core/ipc.js +56 -0
- package/packages/mailx-core/package.json +18 -0
- package/packages/mailx-imap/index.d.ts +7 -1
- package/packages/mailx-imap/index.js +89 -14
- package/packages/mailx-server/index.js +42 -31
- package/packages/mailx-server/package.json +1 -2
- package/packages/mailx-settings/index.d.ts +1 -1
- package/packages/mailx-settings/index.js +21 -12
- package/packages/mailx-store/db.d.ts +6 -2
- package/packages/mailx-store/db.js +78 -16
- package/packages/mailx-store/file-store.d.ts +2 -8
- package/packages/mailx-store/file-store.js +7 -31
- package/packages/mailx-types/index.d.ts +3 -1
- package/.tswalk.json +0 -7396
- package/launcher/release.cmd +0 -4
- package/mailx.json +0 -9
- package/packages/mailx-api/node_modules/nodemailer/.ncurc.js +0 -9
- package/packages/mailx-api/node_modules/nodemailer/.prettierignore +0 -8
- package/packages/mailx-api/node_modules/nodemailer/.prettierrc +0 -12
- package/packages/mailx-api/node_modules/nodemailer/.prettierrc.js +0 -10
- package/packages/mailx-api/node_modules/nodemailer/.release-please-config.json +0 -9
- package/packages/mailx-api/node_modules/nodemailer/LICENSE +0 -16
- package/packages/mailx-api/node_modules/nodemailer/README.md +0 -86
- package/packages/mailx-api/node_modules/nodemailer/SECURITY.txt +0 -22
- package/packages/mailx-api/node_modules/nodemailer/eslint.config.js +0 -88
- package/packages/mailx-api/node_modules/nodemailer/lib/addressparser/index.js +0 -383
- package/packages/mailx-api/node_modules/nodemailer/lib/base64/index.js +0 -139
- package/packages/mailx-api/node_modules/nodemailer/lib/dkim/index.js +0 -253
- package/packages/mailx-api/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
- package/packages/mailx-api/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
- package/packages/mailx-api/node_modules/nodemailer/lib/dkim/sign.js +0 -117
- package/packages/mailx-api/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
- package/packages/mailx-api/node_modules/nodemailer/lib/fetch/index.js +0 -280
- package/packages/mailx-api/node_modules/nodemailer/lib/json-transport/index.js +0 -82
- package/packages/mailx-api/node_modules/nodemailer/lib/mail-composer/index.js +0 -629
- package/packages/mailx-api/node_modules/nodemailer/lib/mailer/index.js +0 -441
- package/packages/mailx-api/node_modules/nodemailer/lib/mailer/mail-message.js +0 -316
- package/packages/mailx-api/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
- package/packages/mailx-api/node_modules/nodemailer/lib/mime-funcs/mime-types.js +0 -2113
- package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/index.js +0 -1316
- package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/last-newline.js +0 -33
- package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/le-unix.js +0 -43
- package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/le-windows.js +0 -52
- package/packages/mailx-api/node_modules/nodemailer/lib/nodemailer.js +0 -157
- package/packages/mailx-api/node_modules/nodemailer/lib/punycode/index.js +0 -460
- package/packages/mailx-api/node_modules/nodemailer/lib/qp/index.js +0 -227
- package/packages/mailx-api/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
- package/packages/mailx-api/node_modules/nodemailer/lib/ses-transport/index.js +0 -234
- package/packages/mailx-api/node_modules/nodemailer/lib/shared/index.js +0 -754
- package/packages/mailx-api/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
- package/packages/mailx-api/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
- package/packages/mailx-api/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1870
- package/packages/mailx-api/node_modules/nodemailer/lib/smtp-pool/index.js +0 -652
- package/packages/mailx-api/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +0 -259
- package/packages/mailx-api/node_modules/nodemailer/lib/smtp-transport/index.js +0 -421
- package/packages/mailx-api/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
- package/packages/mailx-api/node_modules/nodemailer/lib/well-known/index.js +0 -47
- package/packages/mailx-api/node_modules/nodemailer/lib/well-known/services.json +0 -611
- package/packages/mailx-api/node_modules/nodemailer/lib/xoauth2/index.js +0 -427
- package/packages/mailx-api/node_modules/nodemailer/package.json +0 -47
- package/packages/mailx-imap/node_modules/nodemailer/.ncurc.js +0 -9
- package/packages/mailx-imap/node_modules/nodemailer/.prettierignore +0 -8
- package/packages/mailx-imap/node_modules/nodemailer/.prettierrc +0 -12
- package/packages/mailx-imap/node_modules/nodemailer/.prettierrc.js +0 -10
- package/packages/mailx-imap/node_modules/nodemailer/.release-please-config.json +0 -9
- package/packages/mailx-imap/node_modules/nodemailer/LICENSE +0 -16
- package/packages/mailx-imap/node_modules/nodemailer/README.md +0 -86
- package/packages/mailx-imap/node_modules/nodemailer/SECURITY.txt +0 -22
- package/packages/mailx-imap/node_modules/nodemailer/eslint.config.js +0 -88
- package/packages/mailx-imap/node_modules/nodemailer/lib/addressparser/index.js +0 -383
- package/packages/mailx-imap/node_modules/nodemailer/lib/base64/index.js +0 -139
- package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/index.js +0 -253
- package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
- package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
- package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/sign.js +0 -117
- package/packages/mailx-imap/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
- package/packages/mailx-imap/node_modules/nodemailer/lib/fetch/index.js +0 -280
- package/packages/mailx-imap/node_modules/nodemailer/lib/json-transport/index.js +0 -82
- package/packages/mailx-imap/node_modules/nodemailer/lib/mail-composer/index.js +0 -629
- package/packages/mailx-imap/node_modules/nodemailer/lib/mailer/index.js +0 -441
- package/packages/mailx-imap/node_modules/nodemailer/lib/mailer/mail-message.js +0 -316
- package/packages/mailx-imap/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
- package/packages/mailx-imap/node_modules/nodemailer/lib/mime-funcs/mime-types.js +0 -2113
- package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/index.js +0 -1316
- package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/last-newline.js +0 -33
- package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/le-unix.js +0 -43
- package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/le-windows.js +0 -52
- package/packages/mailx-imap/node_modules/nodemailer/lib/nodemailer.js +0 -157
- package/packages/mailx-imap/node_modules/nodemailer/lib/punycode/index.js +0 -460
- package/packages/mailx-imap/node_modules/nodemailer/lib/qp/index.js +0 -227
- package/packages/mailx-imap/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
- package/packages/mailx-imap/node_modules/nodemailer/lib/ses-transport/index.js +0 -234
- package/packages/mailx-imap/node_modules/nodemailer/lib/shared/index.js +0 -754
- package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
- package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
- package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1870
- package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-pool/index.js +0 -652
- package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +0 -259
- package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-transport/index.js +0 -421
- package/packages/mailx-imap/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
- package/packages/mailx-imap/node_modules/nodemailer/lib/well-known/index.js +0 -47
- package/packages/mailx-imap/node_modules/nodemailer/lib/well-known/services.json +0 -611
- package/packages/mailx-imap/node_modules/nodemailer/lib/xoauth2/index.js +0 -427
- package/packages/mailx-imap/node_modules/nodemailer/package.json +0 -47
- package/packages/mailx-send/node_modules/nodemailer/.ncurc.js +0 -9
- package/packages/mailx-send/node_modules/nodemailer/.prettierignore +0 -8
- package/packages/mailx-send/node_modules/nodemailer/.prettierrc +0 -12
- package/packages/mailx-send/node_modules/nodemailer/.prettierrc.js +0 -10
- package/packages/mailx-send/node_modules/nodemailer/.release-please-config.json +0 -9
- package/packages/mailx-send/node_modules/nodemailer/LICENSE +0 -16
- package/packages/mailx-send/node_modules/nodemailer/README.md +0 -86
- package/packages/mailx-send/node_modules/nodemailer/SECURITY.txt +0 -22
- package/packages/mailx-send/node_modules/nodemailer/eslint.config.js +0 -88
- package/packages/mailx-send/node_modules/nodemailer/lib/addressparser/index.js +0 -383
- package/packages/mailx-send/node_modules/nodemailer/lib/base64/index.js +0 -139
- package/packages/mailx-send/node_modules/nodemailer/lib/dkim/index.js +0 -253
- package/packages/mailx-send/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
- package/packages/mailx-send/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
- package/packages/mailx-send/node_modules/nodemailer/lib/dkim/sign.js +0 -117
- package/packages/mailx-send/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
- package/packages/mailx-send/node_modules/nodemailer/lib/fetch/index.js +0 -280
- package/packages/mailx-send/node_modules/nodemailer/lib/json-transport/index.js +0 -82
- package/packages/mailx-send/node_modules/nodemailer/lib/mail-composer/index.js +0 -629
- package/packages/mailx-send/node_modules/nodemailer/lib/mailer/index.js +0 -441
- package/packages/mailx-send/node_modules/nodemailer/lib/mailer/mail-message.js +0 -316
- package/packages/mailx-send/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
- package/packages/mailx-send/node_modules/nodemailer/lib/mime-funcs/mime-types.js +0 -2113
- package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/index.js +0 -1316
- package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/last-newline.js +0 -33
- package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/le-unix.js +0 -43
- package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/le-windows.js +0 -52
- package/packages/mailx-send/node_modules/nodemailer/lib/nodemailer.js +0 -157
- package/packages/mailx-send/node_modules/nodemailer/lib/punycode/index.js +0 -460
- package/packages/mailx-send/node_modules/nodemailer/lib/qp/index.js +0 -227
- package/packages/mailx-send/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
- package/packages/mailx-send/node_modules/nodemailer/lib/ses-transport/index.js +0 -234
- package/packages/mailx-send/node_modules/nodemailer/lib/shared/index.js +0 -754
- package/packages/mailx-send/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
- package/packages/mailx-send/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
- package/packages/mailx-send/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1870
- package/packages/mailx-send/node_modules/nodemailer/lib/smtp-pool/index.js +0 -652
- package/packages/mailx-send/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +0 -259
- package/packages/mailx-send/node_modules/nodemailer/lib/smtp-transport/index.js +0 -421
- package/packages/mailx-send/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
- package/packages/mailx-send/node_modules/nodemailer/lib/well-known/index.js +0 -47
- package/packages/mailx-send/node_modules/nodemailer/lib/well-known/services.json +0 -611
- package/packages/mailx-send/node_modules/nodemailer/lib/xoauth2/index.js +0 -427
- 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
|
-
|
|
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
|
|
58
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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: ${
|
|
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${
|
|
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
|