@zaclinley/dsh-mailharbor 0.15.0 → 0.16.1
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 +31 -0
- package/README.md +36 -35
- package/README.zh.md +35 -33
- package/lib/client.js +236 -266
- package/lib/client.js.map +3 -3
- package/lib/index.js +38 -458
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -120,7 +120,7 @@ function transportOptions(settings, credentials, security = {}) {
|
|
|
120
120
|
secure,
|
|
121
121
|
// Plain submission ports must upgrade; never send credentials in the clear.
|
|
122
122
|
requireTLS: !secure,
|
|
123
|
-
auth:
|
|
123
|
+
auth: { user: credentials.user, pass: credentials.pass ?? "" },
|
|
124
124
|
tls: { rejectUnauthorized: security.allowSelfSigned !== true, servername: settings.host },
|
|
125
125
|
connectionTimeout: 2e4,
|
|
126
126
|
greetingTimeout: 2e4,
|
|
@@ -287,7 +287,7 @@ var init_imap = __esm({
|
|
|
287
287
|
host: this.settings.host,
|
|
288
288
|
port: Number(this.settings.port),
|
|
289
289
|
secure: this.settings.secure !== false,
|
|
290
|
-
auth:
|
|
290
|
+
auth: { user: this.credentials.user, pass: this.credentials.pass ?? "" },
|
|
291
291
|
logger: false,
|
|
292
292
|
tls: { rejectUnauthorized: true },
|
|
293
293
|
connectionTimeout: this.options.connectionTimeoutMs ?? 15e3,
|
|
@@ -710,23 +710,7 @@ var DEFAULTS = {
|
|
|
710
710
|
},
|
|
711
711
|
read: { markOnOpen: true, pollSeconds: 60 },
|
|
712
712
|
render: { listLimit: 40 },
|
|
713
|
-
projects: { root: join2(homedir2(), "work", "projects") }
|
|
714
|
-
oauth: {
|
|
715
|
-
google: {
|
|
716
|
-
clientId: "",
|
|
717
|
-
clientSecret: "",
|
|
718
|
-
redirectPort: 8765,
|
|
719
|
-
scopes: ["https://mail.google.com/"]
|
|
720
|
-
},
|
|
721
|
-
microsoft: {
|
|
722
|
-
clientId: "",
|
|
723
|
-
scopes: [
|
|
724
|
-
"offline_access",
|
|
725
|
-
"https://outlook.office.com/IMAP.AccessAsUser.All",
|
|
726
|
-
"https://outlook.office.com/SMTP.Send"
|
|
727
|
-
]
|
|
728
|
-
}
|
|
729
|
-
}
|
|
713
|
+
projects: { root: join2(homedir2(), "work", "projects") }
|
|
730
714
|
};
|
|
731
715
|
var ACCOUNT_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
732
716
|
var EMAIL = /^[^@\s]+@[^@\s]+$/;
|
|
@@ -776,7 +760,7 @@ function normalizeAccount(input, index = 0, existing = []) {
|
|
|
776
760
|
},
|
|
777
761
|
keychainService: stringOr(input.keychainService, `dsh-mailharbor:${id}`),
|
|
778
762
|
dataDir: stringOr(input.dataDir, `accounts/${id}`),
|
|
779
|
-
auth:
|
|
763
|
+
auth: { type: "password" },
|
|
780
764
|
caldav: normalizeCaldav(input.caldav),
|
|
781
765
|
signature: stringOr(input.signature, "").slice(0, 5e3),
|
|
782
766
|
enabled: input.enabled !== false,
|
|
@@ -831,9 +815,6 @@ function loadConfig() {
|
|
|
831
815
|
const readRaw = file.read ?? {};
|
|
832
816
|
const renderRaw = file.render ?? {};
|
|
833
817
|
const projectsRaw = file.projects ?? {};
|
|
834
|
-
const oauthRaw = file.oauth ?? {};
|
|
835
|
-
const googleRaw = oauthRaw.google ?? {};
|
|
836
|
-
const microsoftRaw = oauthRaw.microsoft ?? {};
|
|
837
818
|
return {
|
|
838
819
|
dataDir: DATA_ROOT,
|
|
839
820
|
configFile: CONFIG_PATH,
|
|
@@ -858,18 +839,6 @@ function loadConfig() {
|
|
|
858
839
|
projects: {
|
|
859
840
|
root: stringOr(projectsRaw.root, DEFAULTS.projects.root)
|
|
860
841
|
},
|
|
861
|
-
oauth: {
|
|
862
|
-
google: {
|
|
863
|
-
clientId: stringOr(googleRaw.clientId, DEFAULTS.oauth.google.clientId),
|
|
864
|
-
clientSecret: stringOr(googleRaw.clientSecret, DEFAULTS.oauth.google.clientSecret),
|
|
865
|
-
redirectPort: intOr(googleRaw.redirectPort, DEFAULTS.oauth.google.redirectPort, 1024, 65535),
|
|
866
|
-
scopes: Array.isArray(googleRaw.scopes) ? googleRaw.scopes.filter((item) => typeof item === "string") : DEFAULTS.oauth.google.scopes
|
|
867
|
-
},
|
|
868
|
-
microsoft: {
|
|
869
|
-
clientId: stringOr(microsoftRaw.clientId, DEFAULTS.oauth.microsoft.clientId),
|
|
870
|
-
scopes: Array.isArray(microsoftRaw.scopes) ? microsoftRaw.scopes.filter((item) => typeof item === "string") : DEFAULTS.oauth.microsoft.scopes
|
|
871
|
-
}
|
|
872
|
-
},
|
|
873
842
|
revision: Date.now()
|
|
874
843
|
};
|
|
875
844
|
}
|
|
@@ -907,7 +876,7 @@ function buildAccount(input, existing) {
|
|
|
907
876
|
username: input.username,
|
|
908
877
|
imap: input.imap,
|
|
909
878
|
smtp: input.smtp,
|
|
910
|
-
auth:
|
|
879
|
+
auth: { type: "password" },
|
|
911
880
|
caldav: null,
|
|
912
881
|
enabled: true,
|
|
913
882
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -1018,26 +987,6 @@ async function openPath(path) {
|
|
|
1018
987
|
}
|
|
1019
988
|
await run("xdg-open", [path]);
|
|
1020
989
|
}
|
|
1021
|
-
async function openExternal(url) {
|
|
1022
|
-
try {
|
|
1023
|
-
const require2 = createRequire(import.meta.url);
|
|
1024
|
-
const electron = require2("electron");
|
|
1025
|
-
if (electron.shell !== void 0) {
|
|
1026
|
-
await electron.shell.openExternal(url);
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
|
-
} catch {
|
|
1030
|
-
}
|
|
1031
|
-
if (process.platform === "darwin") {
|
|
1032
|
-
await run("open", [url]);
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
if (process.platform === "win32") {
|
|
1036
|
-
await run("cmd", ["/c", "start", "", url]);
|
|
1037
|
-
return;
|
|
1038
|
-
}
|
|
1039
|
-
await run("xdg-open", [url]);
|
|
1040
|
-
}
|
|
1041
990
|
|
|
1042
991
|
// src/host/api.ts
|
|
1043
992
|
import { existsSync as existsSync6, rmSync } from "node:fs";
|
|
@@ -1078,134 +1027,6 @@ async function draftMime(account, draft) {
|
|
|
1078
1027
|
return Buffer.from(info.message);
|
|
1079
1028
|
}
|
|
1080
1029
|
|
|
1081
|
-
// src/host/api.ts
|
|
1082
|
-
import { createHash as createHash3, randomBytes } from "node:crypto";
|
|
1083
|
-
import { createServer } from "node:http";
|
|
1084
|
-
|
|
1085
|
-
// src/host/oauth.ts
|
|
1086
|
-
var TOKEN_ENDPOINTS = {
|
|
1087
|
-
google: "https://oauth2.googleapis.com/token",
|
|
1088
|
-
microsoft: "https://login.microsoftonline.com/common/oauth2/v2.0/token"
|
|
1089
|
-
};
|
|
1090
|
-
var DEVICE_ENDPOINTS = {
|
|
1091
|
-
google: "https://oauth2.googleapis.com/device/code",
|
|
1092
|
-
microsoft: "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode"
|
|
1093
|
-
};
|
|
1094
|
-
function form(values) {
|
|
1095
|
-
return new URLSearchParams(values).toString();
|
|
1096
|
-
}
|
|
1097
|
-
function tokensFrom(payload, previousRefresh = "") {
|
|
1098
|
-
const accessToken = typeof payload.access_token === "string" ? payload.access_token : "";
|
|
1099
|
-
if (accessToken === "") throw new Error("OAuth \u54CD\u5E94\u7F3A\u5C11 access_token");
|
|
1100
|
-
const refreshToken = typeof payload.refresh_token === "string" && payload.refresh_token !== "" ? payload.refresh_token : previousRefresh;
|
|
1101
|
-
const expiresIn = Number(payload.expires_in ?? 3600);
|
|
1102
|
-
return {
|
|
1103
|
-
accessToken,
|
|
1104
|
-
refreshToken,
|
|
1105
|
-
expiresAt: Date.now() + Math.max(60, expiresIn) * 1e3,
|
|
1106
|
-
...typeof payload.scope === "string" ? { scope: payload.scope } : {},
|
|
1107
|
-
...typeof payload.token_type === "string" ? { tokenType: payload.token_type } : {}
|
|
1108
|
-
};
|
|
1109
|
-
}
|
|
1110
|
-
async function refreshTokens(app, refreshToken) {
|
|
1111
|
-
if (refreshToken === "") throw new Error("OAuth \u5237\u65B0\u4EE4\u724C\u4E3A\u7A7A");
|
|
1112
|
-
const body = {
|
|
1113
|
-
grant_type: "refresh_token",
|
|
1114
|
-
client_id: app.clientId,
|
|
1115
|
-
refresh_token: refreshToken
|
|
1116
|
-
};
|
|
1117
|
-
if (app.provider === "google") body.client_secret = app.clientSecret;
|
|
1118
|
-
const response = await fetch(TOKEN_ENDPOINTS[app.provider], {
|
|
1119
|
-
method: "POST",
|
|
1120
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
1121
|
-
body: form(body)
|
|
1122
|
-
});
|
|
1123
|
-
const payload = await response.json().catch(() => ({}));
|
|
1124
|
-
if (!response.ok) {
|
|
1125
|
-
const reason = typeof payload.error_description === "string" ? payload.error_description : typeof payload.error === "string" ? payload.error : `HTTP ${String(response.status)}`;
|
|
1126
|
-
throw new Error(`OAuth \u5237\u65B0\u5931\u8D25\uFF1A${reason}`);
|
|
1127
|
-
}
|
|
1128
|
-
return tokensFrom(payload, refreshToken);
|
|
1129
|
-
}
|
|
1130
|
-
function googleAuthUrl(app, state, challenge, redirectUri) {
|
|
1131
|
-
const params = new URLSearchParams({
|
|
1132
|
-
client_id: app.clientId,
|
|
1133
|
-
redirect_uri: redirectUri,
|
|
1134
|
-
response_type: "code",
|
|
1135
|
-
scope: app.scopes.join(" "),
|
|
1136
|
-
access_type: "offline",
|
|
1137
|
-
prompt: "consent",
|
|
1138
|
-
state,
|
|
1139
|
-
code_challenge: challenge,
|
|
1140
|
-
code_challenge_method: "S256"
|
|
1141
|
-
});
|
|
1142
|
-
return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
|
|
1143
|
-
}
|
|
1144
|
-
async function exchangeGoogleCode(app, code, verifier, redirectUri) {
|
|
1145
|
-
const response = await fetch(TOKEN_ENDPOINTS.google, {
|
|
1146
|
-
method: "POST",
|
|
1147
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
1148
|
-
body: form({
|
|
1149
|
-
code,
|
|
1150
|
-
client_id: app.clientId,
|
|
1151
|
-
client_secret: app.clientSecret,
|
|
1152
|
-
redirect_uri: redirectUri,
|
|
1153
|
-
grant_type: "authorization_code",
|
|
1154
|
-
code_verifier: verifier
|
|
1155
|
-
})
|
|
1156
|
-
});
|
|
1157
|
-
const payload = await response.json().catch(() => ({}));
|
|
1158
|
-
if (!response.ok) {
|
|
1159
|
-
throw new Error(
|
|
1160
|
-
`OAuth \u6388\u6743\u5931\u8D25\uFF1A${typeof payload.error_description === "string" ? payload.error_description : `HTTP ${String(response.status)}`}`
|
|
1161
|
-
);
|
|
1162
|
-
}
|
|
1163
|
-
return tokensFrom(payload);
|
|
1164
|
-
}
|
|
1165
|
-
async function startDeviceFlow(app) {
|
|
1166
|
-
const body = {
|
|
1167
|
-
client_id: app.clientId,
|
|
1168
|
-
scope: app.scopes.join(" ")
|
|
1169
|
-
};
|
|
1170
|
-
if (app.provider === "microsoft") body.scope = [...app.scopes, "offline_access"].join(" ");
|
|
1171
|
-
const response = await fetch(DEVICE_ENDPOINTS[app.provider], {
|
|
1172
|
-
method: "POST",
|
|
1173
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
1174
|
-
body: form(body)
|
|
1175
|
-
});
|
|
1176
|
-
const payload = await response.json().catch(() => ({}));
|
|
1177
|
-
if (!response.ok) {
|
|
1178
|
-
throw new Error(
|
|
1179
|
-
`OAuth \u8BBE\u5907\u7801\u8BF7\u6C42\u5931\u8D25\uFF1A${typeof payload.error_description === "string" ? payload.error_description : `HTTP ${String(response.status)}`}`
|
|
1180
|
-
);
|
|
1181
|
-
}
|
|
1182
|
-
return {
|
|
1183
|
-
deviceCode: String(payload.device_code ?? ""),
|
|
1184
|
-
userCode: String(payload.user_code ?? ""),
|
|
1185
|
-
verificationUri: String(payload.verification_uri ?? payload.verification_url ?? ""),
|
|
1186
|
-
interval: Math.max(2, Number(payload.interval ?? 5)),
|
|
1187
|
-
expiresAt: Date.now() + Math.max(60, Number(payload.expires_in ?? 900)) * 1e3
|
|
1188
|
-
};
|
|
1189
|
-
}
|
|
1190
|
-
async function pollDeviceFlow(app, deviceCode) {
|
|
1191
|
-
const response = await fetch(TOKEN_ENDPOINTS[app.provider], {
|
|
1192
|
-
method: "POST",
|
|
1193
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
1194
|
-
body: form({
|
|
1195
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
1196
|
-
client_id: app.clientId,
|
|
1197
|
-
device_code: deviceCode
|
|
1198
|
-
})
|
|
1199
|
-
});
|
|
1200
|
-
const payload = await response.json().catch(() => ({}));
|
|
1201
|
-
if (response.ok) return { status: "ready", tokens: tokensFrom(payload) };
|
|
1202
|
-
const error = String(payload.error ?? "");
|
|
1203
|
-
if (error === "authorization_pending" || error === "slow_down") return { status: "pending" };
|
|
1204
|
-
throw new Error(
|
|
1205
|
-
`OAuth \u8BBE\u5907\u7801\u767B\u5F55\u5931\u8D25\uFF1A${typeof payload.error_description === "string" ? payload.error_description : error}`
|
|
1206
|
-
);
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
1030
|
// src/host/backup.ts
|
|
1210
1031
|
init_address();
|
|
1211
1032
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -1914,7 +1735,7 @@ var BackupEngine = class {
|
|
|
1914
1735
|
host: this.runtime.imap.host,
|
|
1915
1736
|
port: Number(this.runtime.imap.port),
|
|
1916
1737
|
secure: this.runtime.imap.secure !== false,
|
|
1917
|
-
auth:
|
|
1738
|
+
auth: { user: this.credentials.user, pass: this.credentials.pass ?? "" },
|
|
1918
1739
|
logger: false,
|
|
1919
1740
|
tls: { rejectUnauthorized: true },
|
|
1920
1741
|
connectionTimeout: 2e4,
|
|
@@ -2947,7 +2768,7 @@ ${conversation.trim()}
|
|
|
2947
2768
|
}
|
|
2948
2769
|
|
|
2949
2770
|
// src/host/version.ts
|
|
2950
|
-
var VERSION = "0.
|
|
2771
|
+
var VERSION = "0.16.1";
|
|
2951
2772
|
|
|
2952
2773
|
// src/host/api.ts
|
|
2953
2774
|
init_smtp();
|
|
@@ -2998,7 +2819,6 @@ function createApiHandler(runtime) {
|
|
|
2998
2819
|
}
|
|
2999
2820
|
};
|
|
3000
2821
|
}
|
|
3001
|
-
var pendingOAuth = /* @__PURE__ */ new Map();
|
|
3002
2822
|
function buildRouter(runtime) {
|
|
3003
2823
|
const router = new Router();
|
|
3004
2824
|
router.get("/health", () => ({ ok: true, version: VERSION }));
|
|
@@ -3177,13 +2997,6 @@ function buildRouter(runtime) {
|
|
|
3177
2997
|
provider: config.ai.provider,
|
|
3178
2998
|
model: config.ai.model,
|
|
3179
2999
|
summaryLength: config.ai.summaryLength
|
|
3180
|
-
},
|
|
3181
|
-
oauth: {
|
|
3182
|
-
google: {
|
|
3183
|
-
clientId: config.oauth.google.clientId,
|
|
3184
|
-
redirectPort: config.oauth.google.redirectPort
|
|
3185
|
-
},
|
|
3186
|
-
microsoft: { clientId: config.oauth.microsoft.clientId }
|
|
3187
3000
|
}
|
|
3188
3001
|
},
|
|
3189
3002
|
accounts: config.accounts.map((account) => ({
|
|
@@ -3238,155 +3051,9 @@ function buildRouter(runtime) {
|
|
|
3238
3051
|
const ai = asObject(settings.ai);
|
|
3239
3052
|
if (ai.summaryLength !== void 0) patch.ai = { summaryLength: ai.summaryLength };
|
|
3240
3053
|
if (Object.keys(patch).length > 0) saveSettings(patch);
|
|
3241
|
-
const oauth = asObject(settings.oauth);
|
|
3242
|
-
const google = asObject(oauth.google);
|
|
3243
|
-
const microsoft = asObject(oauth.microsoft);
|
|
3244
|
-
if (typeof google.clientId === "string" || typeof microsoft.clientId === "string") {
|
|
3245
|
-
saveSettings({
|
|
3246
|
-
oauth: {
|
|
3247
|
-
google: {
|
|
3248
|
-
clientId: typeof google.clientId === "string" ? google.clientId : config.oauth.google.clientId,
|
|
3249
|
-
clientSecret: config.oauth.google.clientSecret,
|
|
3250
|
-
redirectPort: typeof google.redirectPort === "number" ? google.redirectPort : config.oauth.google.redirectPort,
|
|
3251
|
-
scopes: config.oauth.google.scopes
|
|
3252
|
-
},
|
|
3253
|
-
microsoft: {
|
|
3254
|
-
clientId: typeof microsoft.clientId === "string" ? microsoft.clientId : config.oauth.microsoft.clientId,
|
|
3255
|
-
scopes: config.oauth.microsoft.scopes
|
|
3256
|
-
}
|
|
3257
|
-
}
|
|
3258
|
-
});
|
|
3259
|
-
}
|
|
3260
3054
|
runtime.refresh();
|
|
3261
3055
|
return { ok: true, addedAccounts: added, state: appState(runtime) };
|
|
3262
3056
|
});
|
|
3263
|
-
router.get("/oauth/status", ({ url }) => {
|
|
3264
|
-
const { account } = accountFrom(runtime, url);
|
|
3265
|
-
const tokens = runtime.oauthTokens(account);
|
|
3266
|
-
let appConfigured = false;
|
|
3267
|
-
try {
|
|
3268
|
-
runtime.oauthApp(account);
|
|
3269
|
-
appConfigured = true;
|
|
3270
|
-
} catch {
|
|
3271
|
-
appConfigured = false;
|
|
3272
|
-
}
|
|
3273
|
-
return {
|
|
3274
|
-
authorized: tokens !== void 0,
|
|
3275
|
-
expiresAt: tokens?.expiresAt ?? null,
|
|
3276
|
-
provider: account.auth.type === "oauth2" ? account.auth.provider : null,
|
|
3277
|
-
appConfigured
|
|
3278
|
-
};
|
|
3279
|
-
});
|
|
3280
|
-
router.post("/oauth/start", async ({ body }) => {
|
|
3281
|
-
const payload = await body();
|
|
3282
|
-
const account = accountFromPayload(runtime, payload);
|
|
3283
|
-
const app = runtime.oauthApp(account);
|
|
3284
|
-
const state = randomBytes(16).toString("hex");
|
|
3285
|
-
const expiresAt = Date.now() + 10 * 6e4;
|
|
3286
|
-
if (app.provider === "microsoft") {
|
|
3287
|
-
const device = await startDeviceFlow(app);
|
|
3288
|
-
pendingOAuth.set(state, {
|
|
3289
|
-
accountId: account.id,
|
|
3290
|
-
provider: "microsoft",
|
|
3291
|
-
status: "pending",
|
|
3292
|
-
deviceCode: device.deviceCode,
|
|
3293
|
-
interval: device.interval,
|
|
3294
|
-
expiresAt
|
|
3295
|
-
});
|
|
3296
|
-
return {
|
|
3297
|
-
state,
|
|
3298
|
-
flow: "microsoft",
|
|
3299
|
-
userCode: device.userCode,
|
|
3300
|
-
verificationUri: device.verificationUri,
|
|
3301
|
-
authUrl: device.verificationUri,
|
|
3302
|
-
interval: device.interval
|
|
3303
|
-
};
|
|
3304
|
-
}
|
|
3305
|
-
const verifier = randomBytes(32).toString("base64url");
|
|
3306
|
-
const challenge = createHash3("sha256").update(verifier).digest("base64url");
|
|
3307
|
-
const redirectUri = `http://127.0.0.1:${String(app.redirectPort)}/oauth/callback`;
|
|
3308
|
-
const authUrl = googleAuthUrl(app, state, challenge, redirectUri);
|
|
3309
|
-
const entry = {
|
|
3310
|
-
accountId: account.id,
|
|
3311
|
-
provider: "google",
|
|
3312
|
-
status: "pending",
|
|
3313
|
-
expiresAt
|
|
3314
|
-
};
|
|
3315
|
-
pendingOAuth.set(state, entry);
|
|
3316
|
-
const server = createServer((request, response) => {
|
|
3317
|
-
const target = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
3318
|
-
if (target.pathname !== "/oauth/callback") {
|
|
3319
|
-
response.writeHead(404).end();
|
|
3320
|
-
return;
|
|
3321
|
-
}
|
|
3322
|
-
const code = target.searchParams.get("code");
|
|
3323
|
-
const error = target.searchParams.get("error");
|
|
3324
|
-
void (async () => {
|
|
3325
|
-
try {
|
|
3326
|
-
if (error !== null) throw new Error(error);
|
|
3327
|
-
if (code === null) throw new Error("\u7F3A\u5C11\u6388\u6743\u7801");
|
|
3328
|
-
const tokens = await exchangeGoogleCode(app, code, verifier, redirectUri);
|
|
3329
|
-
runtime.saveOAuthTokens(account, tokens);
|
|
3330
|
-
entry.status = "ready";
|
|
3331
|
-
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
3332
|
-
response.end("<p>MailHarbor \u6388\u6743\u6210\u529F\uFF0C\u53EF\u4EE5\u5173\u95ED\u6B64\u9875\u9762\u3002</p>");
|
|
3333
|
-
} catch (caught) {
|
|
3334
|
-
entry.status = "error";
|
|
3335
|
-
entry.message = caught instanceof Error ? caught.message : String(caught);
|
|
3336
|
-
response.writeHead(500, { "content-type": "text/html; charset=utf-8" });
|
|
3337
|
-
response.end(`<p>\u6388\u6743\u5931\u8D25\uFF1A${entry.message}</p>`);
|
|
3338
|
-
} finally {
|
|
3339
|
-
setTimeout(() => {
|
|
3340
|
-
server.close();
|
|
3341
|
-
}, 500);
|
|
3342
|
-
}
|
|
3343
|
-
})();
|
|
3344
|
-
});
|
|
3345
|
-
await new Promise((resolve, reject) => {
|
|
3346
|
-
server.once("error", reject);
|
|
3347
|
-
server.listen(app.redirectPort, "127.0.0.1", () => resolve());
|
|
3348
|
-
});
|
|
3349
|
-
setTimeout(() => {
|
|
3350
|
-
server.close();
|
|
3351
|
-
if (entry.status === "pending") {
|
|
3352
|
-
entry.status = "error";
|
|
3353
|
-
entry.message = "\u6388\u6743\u8D85\u65F6\uFF0C\u8BF7\u91CD\u8BD5";
|
|
3354
|
-
}
|
|
3355
|
-
}, 10 * 6e4).unref();
|
|
3356
|
-
await openExternal(authUrl).catch(() => void 0);
|
|
3357
|
-
return { state, flow: "google", authUrl, expiresAt };
|
|
3358
|
-
});
|
|
3359
|
-
router.post("/oauth/poll", async ({ body }) => {
|
|
3360
|
-
const payload = await body();
|
|
3361
|
-
const state = String(payload.state ?? "");
|
|
3362
|
-
const entry = pendingOAuth.get(state);
|
|
3363
|
-
if (entry === void 0) throw new HttpError(404, "\u6388\u6743\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u5DF2\u8FC7\u671F");
|
|
3364
|
-
if (Date.now() > entry.expiresAt) {
|
|
3365
|
-
entry.status = "error";
|
|
3366
|
-
entry.message = "\u6388\u6743\u8D85\u65F6\uFF0C\u8BF7\u91CD\u8BD5";
|
|
3367
|
-
}
|
|
3368
|
-
if (entry.status !== "pending") {
|
|
3369
|
-
if (entry.status === "ready") pendingOAuth.delete(state);
|
|
3370
|
-
return { status: entry.status, message: entry.message };
|
|
3371
|
-
}
|
|
3372
|
-
if (entry.provider === "microsoft" && entry.deviceCode !== void 0) {
|
|
3373
|
-
const account = runtime.account(entry.accountId);
|
|
3374
|
-
const result = await pollDeviceFlow(runtime.oauthApp(account), entry.deviceCode);
|
|
3375
|
-
if (result.status === "ready") {
|
|
3376
|
-
runtime.saveOAuthTokens(account, result.tokens);
|
|
3377
|
-
pendingOAuth.delete(state);
|
|
3378
|
-
return { status: "ready" };
|
|
3379
|
-
}
|
|
3380
|
-
}
|
|
3381
|
-
return { status: "pending" };
|
|
3382
|
-
});
|
|
3383
|
-
router.post("/oauth/revoke", async ({ body }) => {
|
|
3384
|
-
const payload = await body();
|
|
3385
|
-
const account = accountFromPayload(runtime, payload);
|
|
3386
|
-
runtime.credentials.forgetOAuth(account);
|
|
3387
|
-
runtime.invalidate(account.id);
|
|
3388
|
-
return { ok: true };
|
|
3389
|
-
});
|
|
3390
3057
|
router.get("/messages/thread", ({ url }) => {
|
|
3391
3058
|
const { account } = accountFrom(runtime, url);
|
|
3392
3059
|
const subject = url.searchParams.get("subject") ?? "";
|
|
@@ -3440,14 +3107,26 @@ function buildRouter(runtime) {
|
|
|
3440
3107
|
const part = Number(url.searchParams.get("part") ?? 0);
|
|
3441
3108
|
const provider = runtime.provider(account);
|
|
3442
3109
|
const file2 = await provider.attachment(mailbox, uid, part);
|
|
3443
|
-
writeAttachment(
|
|
3110
|
+
writeAttachment(
|
|
3111
|
+
res,
|
|
3112
|
+
file2.filename,
|
|
3113
|
+
file2.contentType,
|
|
3114
|
+
Buffer.from(file2.dataBase64, "base64"),
|
|
3115
|
+
canPreviewInline(file2.contentType) && url.searchParams.get("inline") === "1"
|
|
3116
|
+
);
|
|
3444
3117
|
return void 0;
|
|
3445
3118
|
}
|
|
3446
3119
|
const emailId = Number(url.searchParams.get("emailId") ?? 0);
|
|
3447
3120
|
const attachmentId = Number(url.searchParams.get("attachmentId") ?? 0);
|
|
3448
3121
|
const file = runtime.database(account).attachment(emailId, attachmentId);
|
|
3449
3122
|
if (file === void 0) throw new HttpError(404, "\u9644\u4EF6\u4E0D\u5B58\u5728");
|
|
3450
|
-
writeAttachment(
|
|
3123
|
+
writeAttachment(
|
|
3124
|
+
res,
|
|
3125
|
+
file.filename,
|
|
3126
|
+
file.contentType,
|
|
3127
|
+
Buffer.from(file.data),
|
|
3128
|
+
canPreviewInline(file.contentType) && url.searchParams.get("inline") === "1"
|
|
3129
|
+
);
|
|
3451
3130
|
return void 0;
|
|
3452
3131
|
});
|
|
3453
3132
|
router.get("/drafts", ({ url }) => {
|
|
@@ -3519,9 +3198,8 @@ function buildRouter(runtime) {
|
|
|
3519
3198
|
const password = typeof input.password === "string" ? input.password : "";
|
|
3520
3199
|
const config = runtime.current;
|
|
3521
3200
|
const account = buildAccount(input, config.accounts);
|
|
3522
|
-
|
|
3523
|
-
if (
|
|
3524
|
-
if (!oauth && input.connect !== false) {
|
|
3201
|
+
if (password === "") throw new HttpError(400, "\u8BF7\u8F93\u5165\u5BC6\u7801\u6216\u5E94\u7528\u4E13\u7528\u5BC6\u7801");
|
|
3202
|
+
if (input.connect !== false) {
|
|
3525
3203
|
const provider = await verifyImap(account, password);
|
|
3526
3204
|
await provider.dispose();
|
|
3527
3205
|
try {
|
|
@@ -3529,7 +3207,7 @@ function buildRouter(runtime) {
|
|
|
3529
3207
|
} catch {
|
|
3530
3208
|
}
|
|
3531
3209
|
}
|
|
3532
|
-
|
|
3210
|
+
runtime.credentials.remember(account, password, input.remember !== false);
|
|
3533
3211
|
const accounts = [...config.accounts, account];
|
|
3534
3212
|
saveAccounts(accounts, account.id);
|
|
3535
3213
|
runtime.refresh();
|
|
@@ -4050,23 +3728,6 @@ function buildRouter(runtime) {
|
|
|
4050
3728
|
if (payload.ai !== void 0) patch.ai = asObject(payload.ai);
|
|
4051
3729
|
if (payload.read !== void 0) patch.read = asObject(payload.read);
|
|
4052
3730
|
if (payload.render !== void 0) patch.render = asObject(payload.render);
|
|
4053
|
-
if (payload.oauth !== void 0) {
|
|
4054
|
-
const oauth = asObject(payload.oauth);
|
|
4055
|
-
const google = asObject(oauth.google);
|
|
4056
|
-
const microsoft = asObject(oauth.microsoft);
|
|
4057
|
-
patch.oauth = {
|
|
4058
|
-
google: {
|
|
4059
|
-
clientId: typeof google.clientId === "string" ? google.clientId.trim() : runtime.current.oauth.google.clientId,
|
|
4060
|
-
clientSecret: typeof google.clientSecret === "string" ? google.clientSecret.trim() : runtime.current.oauth.google.clientSecret,
|
|
4061
|
-
redirectPort: typeof google.redirectPort === "number" ? google.redirectPort : runtime.current.oauth.google.redirectPort,
|
|
4062
|
-
scopes: runtime.current.oauth.google.scopes
|
|
4063
|
-
},
|
|
4064
|
-
microsoft: {
|
|
4065
|
-
clientId: typeof microsoft.clientId === "string" ? microsoft.clientId.trim() : runtime.current.oauth.microsoft.clientId,
|
|
4066
|
-
scopes: runtime.current.oauth.microsoft.scopes
|
|
4067
|
-
}
|
|
4068
|
-
};
|
|
4069
|
-
}
|
|
4070
3731
|
if (payload.projects !== void 0) {
|
|
4071
3732
|
const root = String(asObject(payload.projects).root ?? "").trim();
|
|
4072
3733
|
if (root === "") throw new HttpError(400, "\u9879\u76EE\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A");
|
|
@@ -4215,8 +3876,7 @@ function settingsView(config) {
|
|
|
4215
3876
|
},
|
|
4216
3877
|
read: { markOnOpen: config.read.markOnOpen, pollSeconds: config.read.pollSeconds },
|
|
4217
3878
|
render: { listLimit: config.render.listLimit },
|
|
4218
|
-
projects: { root: config.projects.root }
|
|
4219
|
-
oauth: config.oauth
|
|
3879
|
+
projects: { root: config.projects.root }
|
|
4220
3880
|
};
|
|
4221
3881
|
}
|
|
4222
3882
|
function taskView(task) {
|
|
@@ -4350,13 +4010,24 @@ async function listServer(runtime, account, mailbox, query, limit, offset = 0) {
|
|
|
4350
4010
|
);
|
|
4351
4011
|
return { items, total: result.total };
|
|
4352
4012
|
}
|
|
4353
|
-
function
|
|
4013
|
+
function canPreviewInline(contentType) {
|
|
4014
|
+
const type = contentType.toLowerCase();
|
|
4015
|
+
return type.startsWith("image/") && type !== "image/svg+xml" || type === "application/pdf" || type === "text/plain" || type === "text/csv" || type === "text/markdown" || type === "application/json";
|
|
4016
|
+
}
|
|
4017
|
+
function writeAttachment(res, filename, contentType, data, inline = false) {
|
|
4354
4018
|
res.statusCode = 200;
|
|
4355
4019
|
res.setHeader("content-type", contentType);
|
|
4356
4020
|
res.setHeader("content-length", String(data.length));
|
|
4357
|
-
res.setHeader(
|
|
4021
|
+
res.setHeader(
|
|
4022
|
+
"content-disposition",
|
|
4023
|
+
`${inline ? "inline" : "attachment"}; filename*=UTF-8''${encodeURIComponent(filename)}`
|
|
4024
|
+
);
|
|
4358
4025
|
res.setHeader("cache-control", "no-store");
|
|
4359
4026
|
res.setHeader("x-content-type-options", "nosniff");
|
|
4027
|
+
res.setHeader(
|
|
4028
|
+
"content-security-policy",
|
|
4029
|
+
"sandbox; default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'"
|
|
4030
|
+
);
|
|
4360
4031
|
res.end(data);
|
|
4361
4032
|
}
|
|
4362
4033
|
function asObject(value) {
|
|
@@ -4593,32 +4264,6 @@ var CredentialManager = class {
|
|
|
4593
4264
|
this.session.delete(account.id);
|
|
4594
4265
|
this.secrets.delete(account.keychainService, account.email);
|
|
4595
4266
|
}
|
|
4596
|
-
oauthService(account) {
|
|
4597
|
-
return `${account.keychainService}:oauth`;
|
|
4598
|
-
}
|
|
4599
|
-
readOAuth(account) {
|
|
4600
|
-
const raw = this.secrets.read(this.oauthService(account), account.email);
|
|
4601
|
-
if (raw === void 0 || raw.trim() === "") return void 0;
|
|
4602
|
-
try {
|
|
4603
|
-
const parsed = JSON.parse(raw);
|
|
4604
|
-
if (typeof parsed.accessToken !== "string" || typeof parsed.refreshToken !== "string") return void 0;
|
|
4605
|
-
return {
|
|
4606
|
-
accessToken: parsed.accessToken,
|
|
4607
|
-
refreshToken: parsed.refreshToken,
|
|
4608
|
-
expiresAt: Number(parsed.expiresAt ?? 0),
|
|
4609
|
-
...typeof parsed.scope === "string" ? { scope: parsed.scope } : {},
|
|
4610
|
-
...typeof parsed.tokenType === "string" ? { tokenType: parsed.tokenType } : {}
|
|
4611
|
-
};
|
|
4612
|
-
} catch {
|
|
4613
|
-
return void 0;
|
|
4614
|
-
}
|
|
4615
|
-
}
|
|
4616
|
-
writeOAuth(account, tokens) {
|
|
4617
|
-
return this.secrets.write(this.oauthService(account), account.email, JSON.stringify(tokens));
|
|
4618
|
-
}
|
|
4619
|
-
forgetOAuth(account) {
|
|
4620
|
-
this.secrets.delete(this.oauthService(account), account.email);
|
|
4621
|
-
}
|
|
4622
4267
|
/** Read an arbitrary secret (for example a CalDAV app password). */
|
|
4623
4268
|
readSecret(service, account) {
|
|
4624
4269
|
return this.secrets.read(service, account);
|
|
@@ -5409,69 +5054,13 @@ var MailHarborRuntime = class {
|
|
|
5409
5054
|
paths(account) {
|
|
5410
5055
|
return accountRuntime(account);
|
|
5411
5056
|
}
|
|
5412
|
-
/** OAuth app configuration for one account's provider. */
|
|
5413
|
-
oauthApp(account) {
|
|
5414
|
-
if (account.auth.type !== "oauth2") throw new Error("\u8FD9\u4E2A\u8D26\u53F7\u4E0D\u662F OAuth2 \u8D26\u53F7");
|
|
5415
|
-
const provider = account.auth.provider;
|
|
5416
|
-
const settings = this.current.oauth;
|
|
5417
|
-
if (provider === "google") {
|
|
5418
|
-
if (settings.google.clientId === "") throw new Error("\u8BF7\u5148\u5728\u8BBE\u7F6E\u91CC\u586B\u5199 Google OAuth Client ID");
|
|
5419
|
-
return {
|
|
5420
|
-
provider,
|
|
5421
|
-
clientId: settings.google.clientId,
|
|
5422
|
-
clientSecret: settings.google.clientSecret,
|
|
5423
|
-
redirectPort: settings.google.redirectPort,
|
|
5424
|
-
scopes: settings.google.scopes.length > 0 ? settings.google.scopes : ["https://mail.google.com/"]
|
|
5425
|
-
};
|
|
5426
|
-
}
|
|
5427
|
-
if (settings.microsoft.clientId === "") throw new Error("\u8BF7\u5148\u5728\u8BBE\u7F6E\u91CC\u586B\u5199 Microsoft OAuth Client ID");
|
|
5428
|
-
return {
|
|
5429
|
-
provider,
|
|
5430
|
-
clientId: settings.microsoft.clientId,
|
|
5431
|
-
clientSecret: "",
|
|
5432
|
-
redirectPort: 0,
|
|
5433
|
-
scopes: settings.microsoft.scopes.length > 0 ? settings.microsoft.scopes : [
|
|
5434
|
-
"offline_access",
|
|
5435
|
-
"https://outlook.office.com/IMAP.AccessAsUser.All",
|
|
5436
|
-
"https://outlook.office.com/SMTP.Send"
|
|
5437
|
-
]
|
|
5438
|
-
};
|
|
5439
|
-
}
|
|
5440
|
-
oauthTokens(account) {
|
|
5441
|
-
return this.credentials.readOAuth(account);
|
|
5442
|
-
}
|
|
5443
|
-
saveOAuthTokens(account, tokens) {
|
|
5444
|
-
this.credentials.writeOAuth(account, tokens);
|
|
5445
|
-
this.invalidate(account.id);
|
|
5446
|
-
}
|
|
5447
|
-
/** Refresh when the access token is close to expiring; safe to call often. */
|
|
5448
|
-
async refreshOAuth(account) {
|
|
5449
|
-
if (account.auth.type !== "oauth2") return void 0;
|
|
5450
|
-
const tokens = this.credentials.readOAuth(account);
|
|
5451
|
-
if (tokens === void 0) return void 0;
|
|
5452
|
-
if (tokens.expiresAt - Date.now() > 10 * 6e4) return tokens;
|
|
5453
|
-
const refreshed = await refreshTokens(this.oauthApp(account), tokens.refreshToken);
|
|
5454
|
-
this.saveOAuthTokens(account, refreshed);
|
|
5455
|
-
return refreshed;
|
|
5456
|
-
}
|
|
5457
5057
|
/** Credentials for an account, with a clear message when the user must log in. */
|
|
5458
5058
|
credentialsFor(account) {
|
|
5459
|
-
if (account.auth.type === "oauth2") {
|
|
5460
|
-
const tokens = this.credentials.readOAuth(account);
|
|
5461
|
-
if (tokens === void 0) throw new Error("\u8BF7\u5148\u5B8C\u6210 OAuth \u6388\u6743");
|
|
5462
|
-
if (tokens.expiresAt !== 0 && tokens.expiresAt <= Date.now()) {
|
|
5463
|
-
throw new Error("OAuth \u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u6388\u6743");
|
|
5464
|
-
}
|
|
5465
|
-
return { user: account.username || account.email, accessToken: tokens.accessToken };
|
|
5466
|
-
}
|
|
5467
5059
|
const resolved = this.credentials.resolve(account);
|
|
5468
5060
|
if (resolved === void 0) throw new Error("\u8BF7\u5148\u767B\u5F55\u90AE\u7BB1\u8D26\u53F7");
|
|
5469
5061
|
return { user: account.username || account.email, pass: resolved.password };
|
|
5470
5062
|
}
|
|
5471
5063
|
hasCredentials(account) {
|
|
5472
|
-
if (account.auth.type === "oauth2") {
|
|
5473
|
-
return this.credentials.readOAuth(account) !== void 0;
|
|
5474
|
-
}
|
|
5475
5064
|
return this.credentials.resolve(account) !== void 0;
|
|
5476
5065
|
}
|
|
5477
5066
|
imapSettings(account) {
|
|
@@ -6034,15 +5623,6 @@ function apply(ctx) {
|
|
|
6034
5623
|
}
|
|
6035
5624
|
const handler = createApiHandler(runtime);
|
|
6036
5625
|
const disposers = [];
|
|
6037
|
-
const refreshOAuth = () => {
|
|
6038
|
-
for (const account of runtime.current.accounts) {
|
|
6039
|
-
if (account.auth.type !== "oauth2" || !runtime.hasCredentials(account)) continue;
|
|
6040
|
-
void runtime.refreshOAuth(account).catch(() => void 0);
|
|
6041
|
-
}
|
|
6042
|
-
};
|
|
6043
|
-
refreshOAuth();
|
|
6044
|
-
const oauthTimer = setInterval(refreshOAuth, 15 * 6e4);
|
|
6045
|
-
disposers.push(() => clearInterval(oauthTimer));
|
|
6046
5626
|
const start = () => {
|
|
6047
5627
|
disposers.push(ctx.webServer.register({ kind: "prefix", path: ROUTE, handler }));
|
|
6048
5628
|
if (ctx.tools !== void 0) disposers.push(...registerMailTools(ctx.tools, runtime));
|