@zaclinley/dsh-mailharbor 0.15.0 → 0.16.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 +18 -0
- package/README.md +36 -35
- package/README.zh.md +35 -33
- package/lib/client.js +109 -234
- package/lib/client.js.map +2 -2
- package/lib/index.js +11 -454
- 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.0";
|
|
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") ?? "";
|
|
@@ -3519,9 +3186,8 @@ function buildRouter(runtime) {
|
|
|
3519
3186
|
const password = typeof input.password === "string" ? input.password : "";
|
|
3520
3187
|
const config = runtime.current;
|
|
3521
3188
|
const account = buildAccount(input, config.accounts);
|
|
3522
|
-
|
|
3523
|
-
if (
|
|
3524
|
-
if (!oauth && input.connect !== false) {
|
|
3189
|
+
if (password === "") throw new HttpError(400, "\u8BF7\u8F93\u5165\u5BC6\u7801\u6216\u5E94\u7528\u4E13\u7528\u5BC6\u7801");
|
|
3190
|
+
if (input.connect !== false) {
|
|
3525
3191
|
const provider = await verifyImap(account, password);
|
|
3526
3192
|
await provider.dispose();
|
|
3527
3193
|
try {
|
|
@@ -3529,7 +3195,7 @@ function buildRouter(runtime) {
|
|
|
3529
3195
|
} catch {
|
|
3530
3196
|
}
|
|
3531
3197
|
}
|
|
3532
|
-
|
|
3198
|
+
runtime.credentials.remember(account, password, input.remember !== false);
|
|
3533
3199
|
const accounts = [...config.accounts, account];
|
|
3534
3200
|
saveAccounts(accounts, account.id);
|
|
3535
3201
|
runtime.refresh();
|
|
@@ -4050,23 +3716,6 @@ function buildRouter(runtime) {
|
|
|
4050
3716
|
if (payload.ai !== void 0) patch.ai = asObject(payload.ai);
|
|
4051
3717
|
if (payload.read !== void 0) patch.read = asObject(payload.read);
|
|
4052
3718
|
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
3719
|
if (payload.projects !== void 0) {
|
|
4071
3720
|
const root = String(asObject(payload.projects).root ?? "").trim();
|
|
4072
3721
|
if (root === "") throw new HttpError(400, "\u9879\u76EE\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A");
|
|
@@ -4215,8 +3864,7 @@ function settingsView(config) {
|
|
|
4215
3864
|
},
|
|
4216
3865
|
read: { markOnOpen: config.read.markOnOpen, pollSeconds: config.read.pollSeconds },
|
|
4217
3866
|
render: { listLimit: config.render.listLimit },
|
|
4218
|
-
projects: { root: config.projects.root }
|
|
4219
|
-
oauth: config.oauth
|
|
3867
|
+
projects: { root: config.projects.root }
|
|
4220
3868
|
};
|
|
4221
3869
|
}
|
|
4222
3870
|
function taskView(task) {
|
|
@@ -4593,32 +4241,6 @@ var CredentialManager = class {
|
|
|
4593
4241
|
this.session.delete(account.id);
|
|
4594
4242
|
this.secrets.delete(account.keychainService, account.email);
|
|
4595
4243
|
}
|
|
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
4244
|
/** Read an arbitrary secret (for example a CalDAV app password). */
|
|
4623
4245
|
readSecret(service, account) {
|
|
4624
4246
|
return this.secrets.read(service, account);
|
|
@@ -5409,69 +5031,13 @@ var MailHarborRuntime = class {
|
|
|
5409
5031
|
paths(account) {
|
|
5410
5032
|
return accountRuntime(account);
|
|
5411
5033
|
}
|
|
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
5034
|
/** Credentials for an account, with a clear message when the user must log in. */
|
|
5458
5035
|
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
5036
|
const resolved = this.credentials.resolve(account);
|
|
5468
5037
|
if (resolved === void 0) throw new Error("\u8BF7\u5148\u767B\u5F55\u90AE\u7BB1\u8D26\u53F7");
|
|
5469
5038
|
return { user: account.username || account.email, pass: resolved.password };
|
|
5470
5039
|
}
|
|
5471
5040
|
hasCredentials(account) {
|
|
5472
|
-
if (account.auth.type === "oauth2") {
|
|
5473
|
-
return this.credentials.readOAuth(account) !== void 0;
|
|
5474
|
-
}
|
|
5475
5041
|
return this.credentials.resolve(account) !== void 0;
|
|
5476
5042
|
}
|
|
5477
5043
|
imapSettings(account) {
|
|
@@ -6034,15 +5600,6 @@ function apply(ctx) {
|
|
|
6034
5600
|
}
|
|
6035
5601
|
const handler = createApiHandler(runtime);
|
|
6036
5602
|
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
5603
|
const start = () => {
|
|
6047
5604
|
disposers.push(ctx.webServer.register({ kind: "prefix", path: ROUTE, handler }));
|
|
6048
5605
|
if (ctx.tools !== void 0) disposers.push(...registerMailTools(ctx.tools, runtime));
|