@zaclinley/dsh-mailharbor 0.13.0 → 0.14.0
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/CHANGELOG.md +28 -0
- package/lib/client.js +115 -33
- package/lib/client.js.map +2 -2
- package/lib/index.js +181 -14
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -128,14 +128,14 @@ function transportOptions(settings, credentials, security = {}) {
|
|
|
128
128
|
pool: false
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
|
-
function
|
|
131
|
+
function createTransport2(settings, credentials, security = {}) {
|
|
132
132
|
return nodemailer.createTransport(transportOptions(settings, credentials, security));
|
|
133
133
|
}
|
|
134
134
|
function recipientsOf(value) {
|
|
135
135
|
return value.split(/[,;]/).map((part) => addressOf(part)).filter((address) => isEmail(address));
|
|
136
136
|
}
|
|
137
137
|
async function verifySmtp(settings, credentials, security = {}) {
|
|
138
|
-
const transport =
|
|
138
|
+
const transport = createTransport2(settings, credentials, security);
|
|
139
139
|
try {
|
|
140
140
|
await transport.verify();
|
|
141
141
|
return { ok: true, host: settings.host, port: Number(settings.port) };
|
|
@@ -149,7 +149,7 @@ async function sendMail(settings, credentials, account, mail, security = {}) {
|
|
|
149
149
|
const to = recipientsOf(mail.to);
|
|
150
150
|
const cc = recipientsOf(mail.cc ?? "");
|
|
151
151
|
if (to.length === 0) throw new Error("\u6536\u4EF6\u4EBA\u4E0D\u80FD\u4E3A\u7A7A");
|
|
152
|
-
const transport =
|
|
152
|
+
const transport = createTransport2(settings, credentials, security);
|
|
153
153
|
try {
|
|
154
154
|
const info = await transport.sendMail({
|
|
155
155
|
from: settings.fromName === "" ? from : { name: settings.fromName, address: from },
|
|
@@ -572,6 +572,28 @@ var init_imap = __esm({
|
|
|
572
572
|
}
|
|
573
573
|
});
|
|
574
574
|
}
|
|
575
|
+
/** Append a message to the account's Drafts mailbox. */
|
|
576
|
+
async appendDraft(raw) {
|
|
577
|
+
const boxes = await this.mailboxes();
|
|
578
|
+
const box = boxes.find((item) => String(item.specialUse ?? "").toLowerCase() === "\\drafts") ?? boxes.find((item) => /drafts|草稿/i.test(item.name)) ?? boxes.find((item) => item.name.toLowerCase() === "drafts");
|
|
579
|
+
const path = box?.path ?? "Drafts";
|
|
580
|
+
return this.run(async (client) => {
|
|
581
|
+
const result = await client.append(path, raw, ["\\Draft"]);
|
|
582
|
+
if (result === false) throw new Error("\u670D\u52A1\u5668\u62D2\u7EDD\u4FDD\u5B58\u8349\u7A3F");
|
|
583
|
+
return { mailbox: path, uid: Number(result.uid) };
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
/** Delete one message (used when replacing a mirrored draft). */
|
|
587
|
+
async deleteMessage(mailbox, uid) {
|
|
588
|
+
return this.run(async (client) => {
|
|
589
|
+
const lock = await client.getMailboxLock(mailbox);
|
|
590
|
+
try {
|
|
591
|
+
await client.messageDelete(String(uid), { uid: true });
|
|
592
|
+
} finally {
|
|
593
|
+
lock.release();
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
}
|
|
575
597
|
/** Move one message to another mailbox (server-side). */
|
|
576
598
|
async move(mailbox, uid, target) {
|
|
577
599
|
return this.run(async (client) => {
|
|
@@ -751,6 +773,7 @@ function normalizeAccount(input, index = 0, existing = []) {
|
|
|
751
773
|
dataDir: stringOr(input.dataDir, `accounts/${id}`),
|
|
752
774
|
auth: input.auth?.type === "oauth2" ? { type: "oauth2", provider: input.auth.provider } : { type: "password" },
|
|
753
775
|
caldav: normalizeCaldav(input.caldav),
|
|
776
|
+
signature: stringOr(input.signature, "").slice(0, 5e3),
|
|
754
777
|
enabled: input.enabled !== false,
|
|
755
778
|
createdAt: stringOr(input.createdAt, (/* @__PURE__ */ new Date()).toISOString())
|
|
756
779
|
};
|
|
@@ -990,6 +1013,9 @@ async function openPath(path) {
|
|
|
990
1013
|
await run("xdg-open", [path]);
|
|
991
1014
|
}
|
|
992
1015
|
|
|
1016
|
+
// src/host/api.ts
|
|
1017
|
+
import { existsSync as existsSync6, rmSync } from "node:fs";
|
|
1018
|
+
|
|
993
1019
|
// src/host/mail-write.ts
|
|
994
1020
|
import { DatabaseSync } from "node:sqlite";
|
|
995
1021
|
function moveLocalMessage(file, id, mailbox) {
|
|
@@ -1002,6 +1028,30 @@ function moveLocalMessage(file, id, mailbox) {
|
|
|
1002
1028
|
}
|
|
1003
1029
|
}
|
|
1004
1030
|
|
|
1031
|
+
// src/host/draft-mime.ts
|
|
1032
|
+
import { createTransport } from "nodemailer";
|
|
1033
|
+
async function draftMime(account, draft) {
|
|
1034
|
+
const transport = createTransport({ streamTransport: true, buffer: true, newline: "unix" });
|
|
1035
|
+
const from = account.smtp.fromName === "" ? account.email : `"${account.smtp.fromName}" <${account.email}>`;
|
|
1036
|
+
const info = await transport.sendMail({
|
|
1037
|
+
from,
|
|
1038
|
+
to: draft.to,
|
|
1039
|
+
cc: draft.cc,
|
|
1040
|
+
bcc: draft.bcc,
|
|
1041
|
+
subject: draft.subject,
|
|
1042
|
+
html: draft.html,
|
|
1043
|
+
attachments: draft.attachments.map((file) => ({
|
|
1044
|
+
filename: file.filename,
|
|
1045
|
+
contentType: file.contentType,
|
|
1046
|
+
content: Buffer.from(file.data, "base64")
|
|
1047
|
+
})),
|
|
1048
|
+
...draft.inReplyTo === void 0 ? {} : { inReplyTo: draft.inReplyTo },
|
|
1049
|
+
...draft.references === void 0 ? {} : { references: draft.references }
|
|
1050
|
+
});
|
|
1051
|
+
if (Buffer.isBuffer(info.message)) return info.message;
|
|
1052
|
+
return Buffer.from(info.message);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1005
1055
|
// src/host/backup.ts
|
|
1006
1056
|
init_address();
|
|
1007
1057
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -1287,6 +1337,50 @@ var MailDatabase = class {
|
|
|
1287
1337
|
const rows = db.prepare(`${columns} FROM emails e ${clause} ORDER BY e.date DESC, e.id DESC LIMIT ?`).all(...params, limit);
|
|
1288
1338
|
return { items: rows.map((row) => rowToSummary(row)), total, offset: 0, limit };
|
|
1289
1339
|
}
|
|
1340
|
+
/** Frequent senders and recipients, for compose autocomplete. */
|
|
1341
|
+
contacts(query = "", limit = 50) {
|
|
1342
|
+
const text2 = query.trim().toLowerCase();
|
|
1343
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1344
|
+
const add = (name2, address) => {
|
|
1345
|
+
const key = address.toLowerCase();
|
|
1346
|
+
if (key === "" || !key.includes("@")) return;
|
|
1347
|
+
const existing = counts.get(key);
|
|
1348
|
+
if (existing === void 0) counts.set(key, { name: name2, address, count: 1 });
|
|
1349
|
+
else {
|
|
1350
|
+
existing.count += 1;
|
|
1351
|
+
if (existing.name === "" && name2 !== "") existing.name = name2;
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
try {
|
|
1355
|
+
const db = this.open();
|
|
1356
|
+
for (const row of db.prepare(
|
|
1357
|
+
`SELECT from_name AS name, from_addr AS address FROM emails
|
|
1358
|
+
WHERE from_addr <> '' AND from_addr LIKE '%@%' ORDER BY date DESC LIMIT 3000`
|
|
1359
|
+
).all()) {
|
|
1360
|
+
add(String(row.name ?? ""), String(row.address ?? ""));
|
|
1361
|
+
}
|
|
1362
|
+
for (const row of db.prepare(
|
|
1363
|
+
`SELECT to_addr AS address, cc AS copies FROM emails
|
|
1364
|
+
WHERE (to_addr <> '' OR cc <> '') ORDER BY date DESC LIMIT 1500`
|
|
1365
|
+
).all()) {
|
|
1366
|
+
for (const entry of parseAddressList(String(row.address ?? ""))) add(entry.name, entry.address);
|
|
1367
|
+
for (const entry of parseAddressList(String(row.copies ?? ""))) add(entry.name, entry.address);
|
|
1368
|
+
}
|
|
1369
|
+
} catch {
|
|
1370
|
+
return [];
|
|
1371
|
+
}
|
|
1372
|
+
return [...counts.values()].filter(
|
|
1373
|
+
(entry) => text2 === "" || entry.address.toLowerCase().includes(text2) || entry.name.toLowerCase().includes(text2)
|
|
1374
|
+
).sort((left, right) => right.count - left.count).slice(0, limit);
|
|
1375
|
+
}
|
|
1376
|
+
/** All archived messages in the same conversation (normalized subject). */
|
|
1377
|
+
thread(subject, limit = 200) {
|
|
1378
|
+
const normalize2 = (value) => value.replace(/^(\s*(re|fw|fwd|回复|转发)\s*[::]\s*)+/gi, "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
1379
|
+
const base = normalize2(subject);
|
|
1380
|
+
if (base === "") return [];
|
|
1381
|
+
const page = this.list({ mailbox: "all", query: base, limit: Math.min(500, limit * 2) });
|
|
1382
|
+
return page.items.filter((item) => normalize2(item.subject) === base).slice(0, limit);
|
|
1383
|
+
}
|
|
1290
1384
|
/** Number of unread keys that still exist in this backup. */
|
|
1291
1385
|
countUnread(keys) {
|
|
1292
1386
|
const mids = keys.mids.slice(0, 500);
|
|
@@ -2699,7 +2793,7 @@ ${conversation.trim()}
|
|
|
2699
2793
|
}
|
|
2700
2794
|
|
|
2701
2795
|
// src/host/version.ts
|
|
2702
|
-
var VERSION = "0.
|
|
2796
|
+
var VERSION = "0.14.0";
|
|
2703
2797
|
|
|
2704
2798
|
// src/host/api.ts
|
|
2705
2799
|
init_smtp();
|
|
@@ -2913,6 +3007,17 @@ function buildRouter(runtime) {
|
|
|
2913
3007
|
}
|
|
2914
3008
|
return { ok: true, count };
|
|
2915
3009
|
});
|
|
3010
|
+
router.get("/messages/thread", ({ url }) => {
|
|
3011
|
+
const { account } = accountFrom(runtime, url);
|
|
3012
|
+
const subject = url.searchParams.get("subject") ?? "";
|
|
3013
|
+
const items = runtime.database(account).thread(subject);
|
|
3014
|
+
return { items: items.map((item) => localSummary(runtime, account, item)) };
|
|
3015
|
+
});
|
|
3016
|
+
router.get("/contacts", ({ url }) => {
|
|
3017
|
+
const { account } = accountFrom(runtime, url);
|
|
3018
|
+
const query = url.searchParams.get("q") ?? "";
|
|
3019
|
+
return { items: runtime.database(account).contacts(query, 50) };
|
|
3020
|
+
});
|
|
2916
3021
|
router.get("/messages/:id", async ({ url, params }) => {
|
|
2917
3022
|
const { account } = accountFrom(runtime, url);
|
|
2918
3023
|
const source = sourceOf(url);
|
|
@@ -2972,13 +3077,40 @@ function buildRouter(runtime) {
|
|
|
2972
3077
|
router.post("/drafts", async ({ url, body }) => {
|
|
2973
3078
|
const { account } = accountFrom(runtime, url);
|
|
2974
3079
|
const payload = await body();
|
|
2975
|
-
const
|
|
2976
|
-
|
|
3080
|
+
const store = runtime.drafts(account);
|
|
3081
|
+
const draft = store.save(normalizeDraftInput(payload, account.id));
|
|
3082
|
+
if (payload.remote !== true) return { draft };
|
|
3083
|
+
if (!runtime.hasCredentials(account)) return { draft, syncError: "\u672A\u767B\u5F55\uFF0C\u8349\u7A3F\u4EC5\u4FDD\u5B58\u5728\u672C\u5730" };
|
|
3084
|
+
try {
|
|
3085
|
+
const raw = await draftMime(account, draft);
|
|
3086
|
+
const provider = runtime.provider(account);
|
|
3087
|
+
const appended = await provider.appendDraft(raw);
|
|
3088
|
+
if (draft.remoteUid !== void 0 && draft.remoteMailbox !== void 0) {
|
|
3089
|
+
try {
|
|
3090
|
+
await provider.deleteMessage(draft.remoteMailbox, draft.remoteUid);
|
|
3091
|
+
} catch {
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
const updated = store.save({ ...draft, remoteUid: appended.uid, remoteMailbox: appended.mailbox });
|
|
3095
|
+
return { draft: updated };
|
|
3096
|
+
} catch (error) {
|
|
3097
|
+
return {
|
|
3098
|
+
draft,
|
|
3099
|
+
syncError: error instanceof Error ? error.message : String(error)
|
|
3100
|
+
};
|
|
3101
|
+
}
|
|
2977
3102
|
});
|
|
2978
|
-
router.delete("/drafts/:id", ({ url, params }) => {
|
|
3103
|
+
router.delete("/drafts/:id", async ({ url, params }) => {
|
|
2979
3104
|
const { account } = accountFrom(runtime, url);
|
|
3105
|
+
const draft = runtime.drafts(account).get(params.id ?? "");
|
|
2980
3106
|
const removed = runtime.drafts(account).remove(params.id ?? "");
|
|
2981
3107
|
if (!removed) throw new HttpError(404, "\u8349\u7A3F\u4E0D\u5B58\u5728");
|
|
3108
|
+
if (draft?.remoteUid !== void 0 && draft.remoteMailbox !== void 0 && runtime.hasCredentials(account)) {
|
|
3109
|
+
try {
|
|
3110
|
+
await runtime.provider(account).deleteMessage(draft.remoteMailbox, draft.remoteUid);
|
|
3111
|
+
} catch {
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
2982
3114
|
return { ok: true };
|
|
2983
3115
|
});
|
|
2984
3116
|
router.post("/send", async ({ url, body }) => {
|
|
@@ -3035,6 +3167,7 @@ function buildRouter(runtime) {
|
|
|
3035
3167
|
imap: { ...existing.imap, ...asObject(payload.imap) },
|
|
3036
3168
|
smtp: { ...existing.smtp, ...asObject(payload.smtp) },
|
|
3037
3169
|
caldav: payload.caldav === null ? null : payload.caldav === void 0 ? existing.caldav : { ...asObject(payload.caldav) },
|
|
3170
|
+
signature: typeof payload.signature === "string" ? payload.signature.slice(0, 5e3) : existing.signature,
|
|
3038
3171
|
enabled: payload.enabled === void 0 ? existing.enabled : payload.enabled !== false
|
|
3039
3172
|
};
|
|
3040
3173
|
const account = normalizeAccount(
|
|
@@ -3443,17 +3576,48 @@ function buildRouter(runtime) {
|
|
|
3443
3576
|
if (project === void 0) throw new HttpError(404, "\u9879\u76EE\u4E0D\u5B58\u5728");
|
|
3444
3577
|
return { project: projectView(project) };
|
|
3445
3578
|
});
|
|
3446
|
-
router.delete("/projects/:id", ({ params }) => {
|
|
3579
|
+
router.delete("/projects/:id", async ({ params, url }) => {
|
|
3447
3580
|
const id = String(params.id ?? "");
|
|
3448
3581
|
const project = runtime.projects().get(id);
|
|
3449
3582
|
if (project === void 0) throw new HttpError(404, "\u9879\u76EE\u4E0D\u5B58\u5728");
|
|
3583
|
+
const warnings = [];
|
|
3450
3584
|
const removed = runtime.projects().remove(id);
|
|
3451
3585
|
const unlinked = runtime.tasks().clearProject(id);
|
|
3586
|
+
let workspaceRemoved = false;
|
|
3587
|
+
if (project.workspaceId !== "" && runtime.workspaceRegistry?.delete !== void 0) {
|
|
3588
|
+
try {
|
|
3589
|
+
workspaceRemoved = await runtime.workspaceRegistry.delete(project.workspaceId);
|
|
3590
|
+
} catch (error) {
|
|
3591
|
+
warnings.push(`\u79FB\u9664\u5DE5\u4F5C\u7A7A\u95F4\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
3592
|
+
}
|
|
3593
|
+
} else if (project.workspaceId !== "") {
|
|
3594
|
+
warnings.push("\u5F53\u524D DSH \u7EC4\u5408\u4E0D\u652F\u6301\u81EA\u52A8\u79FB\u9664\u5DE5\u4F5C\u7A7A\u95F4\uFF0C\u8BF7\u5728\u4FA7\u680F\u624B\u52A8\u5220\u9664");
|
|
3595
|
+
}
|
|
3596
|
+
let filesRemoved = false;
|
|
3597
|
+
if (url.searchParams.get("files") === "1") {
|
|
3598
|
+
try {
|
|
3599
|
+
const root = runtime.current.projects.root;
|
|
3600
|
+
const insideRoot = project.path === root || project.path.startsWith(`${root}/`);
|
|
3601
|
+
const marker = existsSync6(`${project.path}/project.json`);
|
|
3602
|
+
if (!insideRoot && !marker) {
|
|
3603
|
+
warnings.push("\u9879\u76EE\u76EE\u5F55\u4E0D\u5728\u9ED8\u8BA4\u9879\u76EE\u6839\u76EE\u5F55\u4E0B\uFF0C\u51FA\u4E8E\u5B89\u5168\u8003\u8651\u672A\u5220\u9664\u6587\u4EF6");
|
|
3604
|
+
} else if (project.path === root || project.path === "/") {
|
|
3605
|
+
warnings.push("\u62D2\u7EDD\u5220\u9664\u9879\u76EE\u6839\u76EE\u5F55");
|
|
3606
|
+
} else {
|
|
3607
|
+
rmSync(project.path, { recursive: true, force: true });
|
|
3608
|
+
filesRemoved = true;
|
|
3609
|
+
}
|
|
3610
|
+
} catch (error) {
|
|
3611
|
+
warnings.push(`\u5220\u9664\u6587\u4EF6\u5939\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3452
3614
|
return {
|
|
3453
3615
|
ok: removed,
|
|
3454
3616
|
path: project.path,
|
|
3455
3617
|
unlinkedTasks: unlinked,
|
|
3456
|
-
|
|
3618
|
+
workspaceRemoved,
|
|
3619
|
+
filesRemoved,
|
|
3620
|
+
warnings
|
|
3457
3621
|
};
|
|
3458
3622
|
});
|
|
3459
3623
|
router.post("/projects/:id/open", async ({ params }) => {
|
|
@@ -3835,7 +3999,7 @@ async function sendFromPayload(runtime, account, payload) {
|
|
|
3835
3999
|
const totalBytes = attachments.reduce((sum, file) => sum + file.content.length, 0);
|
|
3836
4000
|
if (totalBytes > 15 * 1024 * 1024) throw new HttpError(413, "\u9644\u4EF6\u5408\u8BA1\u4E0D\u80FD\u8D85\u8FC7 15 MB");
|
|
3837
4001
|
const { sendMail: sendMail2 } = await Promise.resolve().then(() => (init_smtp(), smtp_exports));
|
|
3838
|
-
const signature = runtime.current.signature;
|
|
4002
|
+
const signature = (account.signature ?? "").trim() !== "" ? String(account.signature).trim() : runtime.current.signature;
|
|
3839
4003
|
const bodyText2 = signature === "" ? text2 : `${text2}
|
|
3840
4004
|
|
|
3841
4005
|
--
|
|
@@ -4051,7 +4215,7 @@ var CredentialManager = class {
|
|
|
4051
4215
|
};
|
|
4052
4216
|
|
|
4053
4217
|
// src/host/ai-store.ts
|
|
4054
|
-
import { existsSync as
|
|
4218
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4055
4219
|
import { dirname as dirname2 } from "node:path";
|
|
4056
4220
|
var MAX_THREADS = 50;
|
|
4057
4221
|
var MAX_CARDS_PER_THREAD = 50;
|
|
@@ -4066,7 +4230,7 @@ var AiThreadStore = class {
|
|
|
4066
4230
|
if (this.cache !== null) return this.cache;
|
|
4067
4231
|
let parsed = null;
|
|
4068
4232
|
try {
|
|
4069
|
-
if (
|
|
4233
|
+
if (existsSync7(this.file)) parsed = JSON.parse(readFileSync5(this.file, "utf8"));
|
|
4070
4234
|
} catch {
|
|
4071
4235
|
parsed = null;
|
|
4072
4236
|
}
|
|
@@ -4594,7 +4758,9 @@ var DraftStore = class {
|
|
|
4594
4758
|
data: text(file.data)
|
|
4595
4759
|
})) : [],
|
|
4596
4760
|
inReplyTo: text(input.inReplyTo),
|
|
4597
|
-
references: text(input.references)
|
|
4761
|
+
references: text(input.references),
|
|
4762
|
+
...input.remoteUid === void 0 ? existing?.remoteUid === void 0 ? {} : { remoteUid: existing.remoteUid } : { remoteUid: Number(input.remoteUid) },
|
|
4763
|
+
...input.remoteMailbox === void 0 ? existing?.remoteMailbox === void 0 ? {} : { remoteMailbox: existing.remoteMailbox } : { remoteMailbox: text(input.remoteMailbox) }
|
|
4598
4764
|
};
|
|
4599
4765
|
if (existing !== void 0) {
|
|
4600
4766
|
Object.assign(existing, fields, { updatedAt: now2 });
|
|
@@ -4900,12 +5066,13 @@ var MailHarborRuntime = class {
|
|
|
4900
5066
|
const running = this.backups.get(account.id);
|
|
4901
5067
|
if (running !== void 0 && running.status.running) throw new Error("\u5907\u4EFD\u5DF2\u5728\u8FDB\u884C\u4E2D");
|
|
4902
5068
|
const controller = new AbortController();
|
|
5069
|
+
const previous = this.backups.get(account.id)?.status;
|
|
4903
5070
|
const status = {
|
|
4904
5071
|
running: true,
|
|
4905
5072
|
accountId: account.id,
|
|
4906
5073
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4907
5074
|
finishedAt: null,
|
|
4908
|
-
lastSuccessAt: null,
|
|
5075
|
+
lastSuccessAt: previous?.lastSuccessAt ?? null,
|
|
4909
5076
|
total: 0,
|
|
4910
5077
|
added: 0,
|
|
4911
5078
|
message: "\u51C6\u5907\u5907\u4EFD",
|