@zaclinley/dsh-mailharbor 0.0.0-stage → 0.2.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/lib/client.js ADDED
@@ -0,0 +1,2104 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@zaclinley/dsh-mailharbor",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+
7
+ "use strict";
8
+ var __defProp = Object.defineProperty;
9
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
10
+ var __getOwnPropNames = Object.getOwnPropertyNames;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
25
+
26
+ // src/client/index.tsx
27
+ var index_exports = {};
28
+ __export(index_exports, {
29
+ PANEL_ID: () => PANEL_ID,
30
+ VERSION: () => VERSION,
31
+ apply: () => apply,
32
+ inject: () => inject
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // src/client/styles.css
37
+ var styles_default = "/* MailHarbor panel + settings.\n Tokens come from the DSH shell (dark by default); fallbacks match the\n default dark theme so the panel also renders in a bare host page.\n Component rules are all scoped under `.mailharbor` and the generic element\n reset lives in `:where()` so it cannot outrank a component class. */\n\n.mailharbor {\n --m-bg: var(--dsw-alias-bg-base, #151517);\n --m-bg-1: var(--dsw-alias-bg-layer-1, #232324);\n --m-bg-2: var(--dsw-alias-bg-layer-2, #2c2c2e);\n --m-bg-3: var(--dsw-alias-bg-layer-3, #353638);\n --m-line: var(--dsw-alias-border-l2, #ffffff1f);\n --m-line-soft: var(--dsw-alias-border-l1, #ffffff0f);\n --m-line-strong: var(--dsw-alias-border-l3, #ffffff29);\n --m-text: var(--dsw-alias-label-primary, #f9fafb);\n --m-text-2: var(--dsw-alias-label-secondary, #cfd3d6);\n --m-text-3: var(--dsw-alias-label-tertiary, #adb2b8);\n --m-caption: var(--dsw-alias-label-caption, #81858c);\n --m-accent: #5686fe;\n --m-accent-soft: rgba(86, 134, 254, 0.14);\n --m-primary-bg: var(--dsw-alias-brand-primary, #f9fafb);\n --m-primary-fg: var(--dsw-alias-label-primary-foreground, #0f1115);\n --m-hover: var(--dsw-alias-interactive-bg-hover, #ffffff14);\n --m-danger: #f25a5a;\n --m-ok: #3fbf7f;\n --m-warn: var(--dsw-alias-state-warn-label, #dd8629);\n --m-radius: 8px;\n --m-shadow: 0 18px 60px rgba(0, 0, 0, 0.5);\n\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n background: var(--m-bg);\n color: var(--m-text);\n font-size: 13px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n}\n\n.mailharbor *,\n.mailharbor *::before,\n.mailharbor *::after {\n box-sizing: border-box;\n}\n\n/* ---------- base controls ---------- */\n\n.mailharbor :where(button) {\n font: inherit;\n color: inherit;\n background: transparent;\n border: 0;\n margin: 0;\n cursor: pointer;\n}\n\n.mailharbor button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n height: 30px;\n padding: 0 12px;\n border-radius: var(--m-radius);\n border: 1px solid transparent;\n color: var(--m-text-2);\n font-size: 12.5px;\n font-weight: 500;\n white-space: nowrap;\n transition:\n background 120ms ease,\n color 120ms ease,\n border-color 120ms ease;\n}\n\n.mailharbor button:hover:not(:disabled) {\n background: var(--m-hover);\n color: var(--m-text);\n}\n\n.mailharbor button:disabled {\n opacity: 0.45;\n cursor: default;\n}\n\n.mailharbor button.primary {\n background: var(--m-primary-bg);\n color: var(--m-primary-fg);\n border-color: transparent;\n}\n\n.mailharbor button.primary:hover:not(:disabled) {\n background: var(--m-primary-bg);\n filter: brightness(0.94);\n color: var(--m-primary-fg);\n}\n\n.mailharbor button.ghost {\n border-color: var(--m-line);\n color: var(--m-text-2);\n}\n\n.mailharbor button.ghost:hover:not(:disabled) {\n border-color: var(--m-line-strong);\n color: var(--m-text);\n}\n\n.mailharbor button.subtle {\n color: var(--m-text-3);\n padding: 0 8px;\n}\n\n.mailharbor button.icon {\n width: 30px;\n padding: 0;\n}\n\n.mailharbor button.danger {\n color: var(--m-danger);\n}\n\n.mailharbor input,\n.mailharbor select,\n.mailharbor textarea {\n font: inherit;\n color: var(--m-text);\n background: var(--m-bg-1);\n border: 1px solid var(--m-line);\n border-radius: var(--m-radius);\n padding: 0 10px;\n height: 32px;\n min-width: 0;\n}\n\n.mailharbor textarea {\n height: auto;\n padding: 8px 10px;\n resize: vertical;\n line-height: 1.6;\n}\n\n.mailharbor select {\n padding-right: 26px;\n appearance: none;\n background-image:\n linear-gradient(45deg, transparent 50%, var(--m-text-3) 50%),\n linear-gradient(135deg, var(--m-text-3) 50%, transparent 50%);\n background-position:\n calc(100% - 15px) 14px,\n calc(100% - 10px) 14px;\n background-size:\n 5px 5px,\n 5px 5px;\n background-repeat: no-repeat;\n}\n\n.mailharbor ::placeholder {\n color: var(--m-caption);\n}\n\n.mailharbor input:focus-visible,\n.mailharbor select:focus-visible,\n.mailharbor textarea:focus-visible,\n.mailharbor button:focus-visible,\n.mailharbor [contenteditable='true']:focus-visible {\n outline: 2px solid rgba(86, 134, 254, 0.55);\n outline-offset: -1px;\n}\n\n.mailharbor .spacer {\n flex: 1;\n}\n\n.mailharbor .muted {\n color: var(--m-text-3);\n}\n\n.mailharbor .small {\n font-size: 12px;\n}\n\n.mailharbor .badge {\n display: inline-flex;\n align-items: center;\n height: 18px;\n padding: 0 6px;\n border-radius: 999px;\n font-size: 10.5px;\n font-weight: 500;\n background: var(--m-bg-3);\n color: var(--m-text-2);\n}\n\n.mailharbor .badge.ok {\n background: rgba(63, 191, 127, 0.16);\n color: var(--m-ok);\n}\n\n.mailharbor .badge.warn {\n background: rgba(221, 134, 41, 0.16);\n color: var(--m-warn);\n}\n\n.mailharbor .error {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 14px;\n color: var(--m-danger);\n background: rgba(242, 90, 90, 0.1);\n border-bottom: 1px solid rgba(242, 90, 90, 0.2);\n font-size: 12.5px;\n}\n\n/* ---------- shell ---------- */\n\n.mailharbor-topbar {\n display: flex;\n align-items: center;\n gap: 10px;\n height: 52px;\n padding: 0 14px;\n border-bottom: 1px solid var(--m-line-soft);\n flex: none;\n}\n\n.mailharbor-account {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n max-width: 220px;\n padding: 4px 8px 4px 4px;\n border-radius: 999px;\n border: 1px solid transparent;\n cursor: default;\n}\n\n.mailharbor-account:hover {\n background: var(--m-hover);\n}\n\n.mailharbor-account .name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-weight: 500;\n}\n\n.mailharbor-account select {\n height: 26px;\n background: transparent;\n border-color: transparent;\n font-weight: 500;\n}\n\n.mailharbor-body {\n display: flex;\n flex: 1;\n min-height: 0;\n}\n\n.mailharbor-list {\n display: flex;\n flex-direction: column;\n width: 360px;\n flex: none;\n border-right: 1px solid var(--m-line-soft);\n min-height: 0;\n}\n\n.mailharbor-reader {\n flex: 1;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n@media (max-width: 1000px) {\n .mailharbor-list {\n width: 300px;\n }\n}\n\n.mailharbor-narrow .mailharbor-body[data-mode='reader'] .mailharbor-list {\n display: none;\n}\n\n.mailharbor-narrow .mailharbor-body[data-mode='list'] .mailharbor-reader {\n display: none;\n}\n\n/* ---------- segmented control ---------- */\n\n.mailharbor-segment {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n padding: 2px;\n border-radius: 9px;\n background: var(--m-bg-2);\n}\n\n.mailharbor-segment button {\n height: 26px;\n padding: 0 12px;\n border-radius: 7px;\n color: var(--m-text-3);\n font-size: 12px;\n}\n\n.mailharbor-segment button:hover:not(.active) {\n color: var(--m-text-2);\n background: transparent;\n}\n\n.mailharbor-segment button.active {\n background: var(--m-bg-3);\n color: var(--m-text);\n}\n\n/* ---------- list ---------- */\n\n.mailharbor-filters {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 12px;\n border-bottom: 1px solid var(--m-line-soft);\n flex: none;\n}\n\n.mailharbor-filters select {\n width: 128px;\n flex: none;\n font-size: 12px;\n}\n\n.mailharbor-filters input[type='search'] {\n flex: 1;\n height: 30px;\n font-size: 12.5px;\n}\n\n.mailharbor-rows {\n flex: 1;\n overflow: auto;\n min-height: 0;\n}\n\n.mailharbor-row {\n display: flex;\n flex-direction: column;\n gap: 3px;\n width: 100%;\n padding: 10px 14px;\n border: 0;\n border-bottom: 1px solid var(--m-line-soft);\n border-left: 2px solid transparent;\n border-radius: 0;\n background: transparent;\n color: inherit;\n text-align: left;\n cursor: pointer;\n user-select: none;\n}\n\n.mailharbor-row:hover {\n background: var(--m-hover);\n}\n\n.mailharbor-row.selected {\n background: var(--m-accent-soft);\n border-left-color: var(--m-accent);\n}\n\n.mailharbor-row .line-1,\n.mailharbor-row .line-2 {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n width: 100%;\n}\n\n.mailharbor-row .from {\n font-weight: 600;\n font-size: 12.5px;\n color: var(--m-text-2);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.mailharbor-row .date {\n margin-left: auto;\n color: var(--m-caption);\n font-size: 11px;\n flex: none;\n}\n\n.mailharbor-row .subject {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--m-text-2);\n font-size: 12.5px;\n}\n\n.mailharbor-row .snippet {\n color: var(--m-caption);\n font-size: 11.5px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n width: 100%;\n}\n\n.mailharbor-row.unread .from,\n.mailharbor-row.unread .subject {\n color: var(--m-text);\n}\n\n.mailharbor-row .dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--m-accent);\n flex: none;\n}\n\n.mailharbor-row .paperclip {\n color: var(--m-caption);\n flex: none;\n display: inline-flex;\n}\n\n.mailharbor-list-foot {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n border-top: 1px solid var(--m-line-soft);\n color: var(--m-text-3);\n font-size: 11.5px;\n flex: none;\n}\n\n.mailharbor-empty {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 10px;\n padding: 48px 24px;\n color: var(--m-text-3);\n text-align: center;\n}\n\n.mailharbor-empty svg {\n opacity: 0.5;\n}\n\n.mailharbor-empty .title {\n color: var(--m-text-2);\n font-size: 13px;\n font-weight: 500;\n}\n\n/* ---------- reader ---------- */\n\n.mailharbor-reader-head {\n padding: 16px 20px 12px;\n border-bottom: 1px solid var(--m-line-soft);\n flex: none;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.mailharbor-reader-title {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n}\n\n.mailharbor-reader-title h2 {\n margin: 0;\n font-size: 17px;\n line-height: 1.35;\n font-weight: 600;\n letter-spacing: -0.01em;\n word-break: break-word;\n}\n\n.mailharbor-reader-meta {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 4px 10px;\n color: var(--m-text-3);\n font-size: 12px;\n}\n\n.mailharbor-reader-meta .who {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n color: var(--m-text-2);\n}\n\n.mailharbor-reader-meta .who strong {\n color: var(--m-text);\n font-weight: 600;\n}\n\n.mailharbor-reader-actions {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-wrap: wrap;\n}\n\n.mailharbor-reader-body {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n padding: 14px 18px 18px;\n background: var(--m-bg-2);\n}\n\n.mailharbor-reader-body iframe {\n flex: 1;\n width: 100%;\n border: 0;\n border-radius: 10px;\n background: #fff;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);\n}\n\n.mailharbor-reader-body pre {\n flex: 1;\n overflow: auto;\n margin: 0;\n padding: 20px 22px;\n white-space: pre-wrap;\n word-break: break-word;\n background: #fff;\n border-radius: 10px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);\n color: #171a21;\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 12.5px;\n line-height: 1.7;\n}\n\n.mailharbor-attachments {\n padding: 10px 18px;\n border-top: 1px solid var(--m-line-soft);\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n flex: none;\n background: var(--m-bg);\n}\n\n.mailharbor-attachment {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n height: 30px;\n padding: 0 10px;\n border: 1px solid var(--m-line);\n border-radius: var(--m-radius);\n font-size: 12px;\n text-decoration: none;\n color: var(--m-text-2);\n max-width: 260px;\n background: var(--m-bg-1);\n}\n\n.mailharbor-attachment:hover {\n border-color: var(--m-line-strong);\n color: var(--m-text);\n}\n\n.mailharbor-attachment .name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* ---------- modal / forms ---------- */\n\n.mailharbor-portal {\n position: fixed;\n inset: 0;\n z-index: 80;\n height: auto;\n background: transparent;\n pointer-events: none;\n}\n\n.mailharbor-portal .mailharbor-modal-mask {\n pointer-events: auto;\n}\n\n.mailharbor-modal-mask {\n position: fixed;\n inset: 0;\n z-index: 60;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: rgba(0, 0, 0, 0.55);\n backdrop-filter: blur(2px);\n}\n\n.mailharbor-modal {\n display: flex;\n flex-direction: column;\n width: min(720px, 100%);\n max-height: 86vh;\n overflow: hidden;\n border: 1px solid var(--m-line);\n border-radius: 14px;\n background: var(--m-bg-1);\n color: var(--m-text);\n box-shadow: var(--m-shadow);\n}\n\n.mailharbor-modal > header {\n display: flex;\n align-items: center;\n gap: 8px;\n height: 52px;\n padding: 0 14px 0 18px;\n border-bottom: 1px solid var(--m-line-soft);\n font-size: 14px;\n font-weight: 600;\n}\n\n.mailharbor-modal .content {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 18px;\n overflow: auto;\n}\n\n.mailharbor-modal > footer {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 18px;\n border-top: 1px solid var(--m-line-soft);\n}\n\n.mailharbor-field {\n display: flex;\n flex-direction: column;\n gap: 5px;\n min-width: 0;\n}\n\n.mailharbor-field > label {\n font-size: 11.5px;\n color: var(--m-text-3);\n}\n\n.mailharbor-grid {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 10px;\n}\n\n.mailharbor-grid.three {\n grid-template-columns: 2fr 90px auto;\n align-items: end;\n}\n\n.mailharbor-toggle {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n color: var(--m-text-2);\n font-size: 12.5px;\n cursor: pointer;\n white-space: nowrap;\n}\n\n.mailharbor-toggle input[type='checkbox'] {\n width: 14px;\n height: 14px;\n accent-color: var(--m-accent);\n}\n\n/* ---------- composer ---------- */\n\n.mailharbor-toolbar {\n display: flex;\n align-items: center;\n gap: 2px;\n flex-wrap: wrap;\n padding: 6px;\n border: 1px solid var(--m-line-soft);\n border-bottom: 0;\n border-radius: 10px 10px 0 0;\n background: var(--m-bg-2);\n}\n\n.mailharbor-toolbar button {\n height: 26px;\n padding: 0 9px;\n border-radius: 6px;\n color: var(--m-text-2);\n font-size: 12px;\n}\n\n.mailharbor-toolbar button:hover {\n background: var(--m-bg-3);\n color: var(--m-text);\n}\n\n.mailharbor-editor {\n min-height: 240px;\n max-height: 44vh;\n overflow: auto;\n padding: 14px 16px;\n border: 1px solid var(--m-line-soft);\n border-radius: 0 0 10px 10px;\n background: #fff;\n color: #171a21;\n outline: none;\n font-size: 13.5px;\n line-height: 1.7;\n}\n\n.mailharbor-editor:empty::before {\n content: attr(data-placeholder);\n color: #9aa0ab;\n}\n\n/* ---------- settings ---------- */\n\n.mailharbor-settings {\n padding: 20px 24px 48px;\n display: flex;\n flex-direction: column;\n gap: 16px;\n max-width: 880px;\n}\n\n.mailharbor-settings h2 {\n margin: 0;\n font-size: 18px;\n font-weight: 600;\n letter-spacing: -0.01em;\n}\n\n.mailharbor-settings h3 {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--m-text-3);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n}\n\n.mailharbor-card {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 16px;\n border: 1px solid var(--m-line-soft);\n border-radius: 12px;\n background: var(--m-bg-1);\n}\n\n.mailharbor-account-row {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.mailharbor-account-row .meta {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n.mailharbor-account-row .meta strong {\n font-weight: 600;\n}\n\n.mailharbor-account-row .meta span {\n color: var(--m-text-3);\n font-size: 11.5px;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.mailharbor .avatar {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n border-radius: 50%;\n background: var(--m-accent-soft);\n color: var(--m-accent);\n font-weight: 600;\n}\n\n.mailharbor-log {\n padding: 10px 12px;\n border-radius: 8px;\n background: var(--m-bg);\n border: 1px solid var(--m-line-soft);\n color: var(--m-text-2);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 11px;\n line-height: 1.6;\n max-height: 220px;\n overflow: auto;\n white-space: pre-wrap;\n}\n\n.mailharbor-kv {\n display: grid;\n grid-template-columns: minmax(120px, 170px) 1fr;\n gap: 6px 14px;\n font-size: 12px;\n margin: 0;\n}\n\n.mailharbor-kv dt {\n color: var(--m-text-3);\n}\n\n.mailharbor-kv dd {\n margin: 0;\n color: var(--m-text-2);\n word-break: break-all;\n}\n\n.mailharbor hr {\n border: 0;\n border-top: 1px solid var(--m-line-soft);\n margin: 2px 0;\n}\n";
38
+
39
+ // src/client/icon.tsx
40
+ var import_react = require("react");
41
+
42
+ // src/client/api.ts
43
+ var BASE = new URL("mail-plugin/api/", document.baseURI).href;
44
+ var ApiError = class extends Error {
45
+ status;
46
+ constructor(message, status) {
47
+ super(message);
48
+ this.status = status;
49
+ this.name = "ApiError";
50
+ }
51
+ };
52
+ async function request(path, init = {}) {
53
+ const response = await fetch(new URL(path, BASE), {
54
+ credentials: "same-origin",
55
+ ...init,
56
+ headers: {
57
+ accept: "application/json",
58
+ ...init.body === void 0 ? {} : { "content-type": "application/json" },
59
+ ...init.headers ?? {}
60
+ }
61
+ });
62
+ if (!response.ok) {
63
+ let message = response.statusText;
64
+ try {
65
+ const body = await response.json();
66
+ if (body !== null && typeof body === "object" && typeof body.error === "string") {
67
+ message = body.error;
68
+ }
69
+ } catch {
70
+ }
71
+ throw new ApiError(message, response.status);
72
+ }
73
+ return await response.json();
74
+ }
75
+ function query(params) {
76
+ const search = new URLSearchParams();
77
+ for (const [key, value] of Object.entries(params)) {
78
+ if (value !== void 0 && value !== null && value !== "") search.set(key, String(value));
79
+ }
80
+ const text = search.toString();
81
+ return text === "" ? "" : `?${text}`;
82
+ }
83
+ var api = {
84
+ state: () => request("state"),
85
+ folders: (params) => request(`folders${query(params)}`),
86
+ messages: (params) => request(`messages${query({ ...params })}`),
87
+ message: (params) => request(`messages/${encodeURIComponent(params.id)}${query({ ...params })}`),
88
+ setRead: (params) => request(
89
+ `messages/${encodeURIComponent(params.id)}/read${query({
90
+ accountId: params.accountId ?? null,
91
+ source: params.source,
92
+ mailbox: params.mailbox
93
+ })}`,
94
+ { method: "POST", body: JSON.stringify({ read: params.read }) }
95
+ ),
96
+ attachmentUrl: (params) => new URL(`attachment${query(params)}`, BASE).href,
97
+ drafts: (accountId) => request(`drafts${query({ accountId: accountId ?? null })}`),
98
+ saveDraft: (accountId, draft) => request(`drafts${query({ accountId: accountId ?? null })}`, {
99
+ method: "POST",
100
+ body: JSON.stringify(draft)
101
+ }),
102
+ deleteDraft: (id, accountId) => request(`drafts/${encodeURIComponent(id)}${query({ accountId: accountId ?? null })}`, {
103
+ method: "DELETE"
104
+ }),
105
+ send: (accountId, payload) => request(`send${query({ accountId: accountId ?? null })}`, {
106
+ method: "POST",
107
+ body: JSON.stringify(payload)
108
+ }),
109
+ unread: (accountId) => request(`unread${query({ accountId: accountId ?? null })}`),
110
+ addAccount: (input) => request("accounts", {
111
+ method: "POST",
112
+ body: JSON.stringify(input)
113
+ }),
114
+ updateAccount: (id, patch) => request(`accounts/${encodeURIComponent(id)}`, {
115
+ method: "PATCH",
116
+ body: JSON.stringify(patch)
117
+ }),
118
+ deleteAccount: (id) => request(`accounts/${encodeURIComponent(id)}`, { method: "DELETE" }),
119
+ activateAccount: (id) => request(`accounts/${encodeURIComponent(id)}/activate`, { method: "POST" }),
120
+ login: (id, password, remember) => request(`accounts/${encodeURIComponent(id)}/login`, {
121
+ method: "POST",
122
+ body: JSON.stringify({ password, remember })
123
+ }),
124
+ logout: (id) => request(`accounts/${encodeURIComponent(id)}/logout`, { method: "POST" }),
125
+ check: (id) => request(`accounts/${encodeURIComponent(id)}/check`, { method: "POST" }),
126
+ settings: () => request("settings"),
127
+ saveSettings: (patch) => request("settings", { method: "PATCH", body: JSON.stringify(patch) }),
128
+ backupStatus: (accountId) => request(`backup/status${query({ accountId: accountId ?? null })}`),
129
+ startBackup: (accountId) => request(`backup/start${query({ accountId: accountId ?? null })}`, {
130
+ method: "POST"
131
+ }),
132
+ stopBackup: (accountId) => request(`backup/stop${query({ accountId: accountId ?? null })}`, {
133
+ method: "POST"
134
+ })
135
+ };
136
+
137
+ // src/client/icon.tsx
138
+ function MailIcon() {
139
+ const [unread, setUnread] = (0, import_react.useState)(0);
140
+ (0, import_react.useEffect)(() => {
141
+ let cancelled = false;
142
+ let timer;
143
+ const poll = async () => {
144
+ try {
145
+ const result = await api.unread();
146
+ if (!cancelled) setUnread(result.unread);
147
+ } catch {
148
+ }
149
+ };
150
+ void poll();
151
+ const schedule = () => {
152
+ if (cancelled) return;
153
+ timer = window.setTimeout(() => {
154
+ void poll().finally(schedule);
155
+ }, 6e4);
156
+ };
157
+ schedule();
158
+ return () => {
159
+ cancelled = true;
160
+ if (timer !== void 0) window.clearTimeout(timer);
161
+ };
162
+ }, []);
163
+ return (0, import_react.createElement)(
164
+ "span",
165
+ {
166
+ style: { position: "relative", display: "inline-flex", alignItems: "center", justifyContent: "center" }
167
+ },
168
+ (0, import_react.createElement)(
169
+ "svg",
170
+ { width: 18, height: 18, viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true" },
171
+ (0, import_react.createElement)("rect", { x: 2.5, y: 5, width: 19, height: 14, rx: 2.5, stroke: "currentColor", strokeWidth: 1.6 }),
172
+ (0, import_react.createElement)("path", {
173
+ d: "M3.5 7l7.6 5.4a1.6 1.6 0 0 0 1.8 0L20.5 7",
174
+ stroke: "currentColor",
175
+ strokeWidth: 1.6,
176
+ strokeLinecap: "round"
177
+ })
178
+ ),
179
+ unread > 0 ? (0, import_react.createElement)(
180
+ "span",
181
+ {
182
+ style: {
183
+ position: "absolute",
184
+ top: "-4px",
185
+ right: "-6px",
186
+ minWidth: "14px",
187
+ height: "14px",
188
+ padding: "0 3px",
189
+ borderRadius: "999px",
190
+ background: "var(--dsw-alias-brand-primary, #4d6bfe)",
191
+ color: "#fff",
192
+ fontSize: "9px",
193
+ lineHeight: "14px",
194
+ textAlign: "center",
195
+ fontWeight: 600
196
+ }
197
+ },
198
+ unread > 99 ? "99+" : String(unread)
199
+ ) : null
200
+ );
201
+ }
202
+
203
+ // src/client/panel.tsx
204
+ var import_react7 = require("react");
205
+
206
+ // src/client/account-form.tsx
207
+ var import_react3 = require("react");
208
+
209
+ // src/client/components.tsx
210
+ var import_react2 = require("react");
211
+ var import_react_dom = require("react-dom");
212
+ function Avatar({ email, size = 26 }) {
213
+ const letter = (email.trim()[0] ?? "?").toUpperCase();
214
+ return (0, import_react2.createElement)(
215
+ "span",
216
+ {
217
+ className: "avatar",
218
+ style: {
219
+ width: `${String(size)}px`,
220
+ height: `${String(size)}px`,
221
+ fontSize: `${String(Math.round(size * 0.42))}px`
222
+ },
223
+ "aria-hidden": "true"
224
+ },
225
+ letter
226
+ );
227
+ }
228
+ function Field({
229
+ label,
230
+ children,
231
+ hint
232
+ }) {
233
+ return (0, import_react2.createElement)(
234
+ "div",
235
+ { className: "mailharbor-field" },
236
+ (0, import_react2.createElement)("label", null, label),
237
+ children,
238
+ hint === void 0 ? null : (0, import_react2.createElement)("span", { className: "small muted" }, hint)
239
+ );
240
+ }
241
+ function Toggle({
242
+ checked,
243
+ onChange,
244
+ label
245
+ }) {
246
+ return (0, import_react2.createElement)(
247
+ "label",
248
+ { className: "mailharbor-toggle" },
249
+ (0, import_react2.createElement)("input", {
250
+ type: "checkbox",
251
+ checked,
252
+ onChange: (event) => {
253
+ onChange(event.target.checked);
254
+ }
255
+ }),
256
+ label
257
+ );
258
+ }
259
+ function portalRoot() {
260
+ const existing = document.getElementById("mailharbor-portal");
261
+ if (existing !== null) return existing;
262
+ const element = document.createElement("div");
263
+ element.id = "mailharbor-portal";
264
+ element.className = "mailharbor mailharbor-portal";
265
+ document.body.append(element);
266
+ return element;
267
+ }
268
+ function Modal({
269
+ title,
270
+ children,
271
+ footer,
272
+ onClose,
273
+ wide = false
274
+ }) {
275
+ (0, import_react2.useEffect)(() => {
276
+ const onKey = (event) => {
277
+ if (event.key === "Escape") onClose();
278
+ };
279
+ window.addEventListener("keydown", onKey);
280
+ return () => {
281
+ window.removeEventListener("keydown", onKey);
282
+ };
283
+ }, [onClose]);
284
+ return (0, import_react_dom.createPortal)(
285
+ (0, import_react2.createElement)(
286
+ "div",
287
+ {
288
+ className: "mailharbor-modal-mask",
289
+ onMouseDown: (event) => {
290
+ if (event.target === event.currentTarget) onClose();
291
+ }
292
+ },
293
+ (0, import_react2.createElement)(
294
+ "div",
295
+ {
296
+ className: "mailharbor-modal",
297
+ style: wide ? { width: "min(920px, 100%)" } : void 0,
298
+ role: "dialog",
299
+ "aria-modal": "true",
300
+ "aria-label": title
301
+ },
302
+ (0, import_react2.createElement)(
303
+ "header",
304
+ null,
305
+ title,
306
+ (0, import_react2.createElement)("span", { className: "spacer" }),
307
+ (0, import_react2.createElement)("button", { className: "subtle", onClick: onClose, "aria-label": "\u5173\u95ED" }, "\u2715")
308
+ ),
309
+ (0, import_react2.createElement)("div", { className: "content" }, children),
310
+ footer === void 0 ? null : (0, import_react2.createElement)("footer", null, footer)
311
+ )
312
+ ),
313
+ portalRoot()
314
+ );
315
+ }
316
+ function ErrorBar({ message, onDismiss }) {
317
+ return (0, import_react2.createElement)(
318
+ "div",
319
+ { className: "error", role: "alert" },
320
+ (0, import_react2.createElement)("span", null, message),
321
+ (0, import_react2.createElement)("span", { className: "spacer" }),
322
+ (0, import_react2.createElement)("button", { className: "subtle", onClick: onDismiss }, "\u5173\u95ED")
323
+ );
324
+ }
325
+ function shortDate(iso) {
326
+ const date = new Date(iso);
327
+ if (Number.isNaN(date.getTime())) return iso.slice(0, 16);
328
+ const now = /* @__PURE__ */ new Date();
329
+ const sameDay = date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate();
330
+ const pad = (value) => String(value).padStart(2, "0");
331
+ if (sameDay) return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
332
+ if (date.getFullYear() === now.getFullYear()) return `${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
333
+ return `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
334
+ }
335
+ function longDate(iso) {
336
+ const date = new Date(iso);
337
+ if (Number.isNaN(date.getTime())) return iso;
338
+ return date.toLocaleString(void 0, {
339
+ year: "numeric",
340
+ month: "2-digit",
341
+ day: "2-digit",
342
+ hour: "2-digit",
343
+ minute: "2-digit"
344
+ });
345
+ }
346
+ function formatBytes(size) {
347
+ if (!Number.isFinite(size) || size <= 0) return "";
348
+ if (size < 1024) return `${String(size)} B`;
349
+ if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
350
+ return `${(size / 1024 / 1024).toFixed(1)} MB`;
351
+ }
352
+
353
+ // src/client/account-form.tsx
354
+ function domainOf(email) {
355
+ return email.split("@")[1] ?? "example.com";
356
+ }
357
+ function errorText(error) {
358
+ return error instanceof ApiError || error instanceof Error ? error.message : String(error);
359
+ }
360
+ function AddAccountForm({
361
+ onDone,
362
+ onCancel
363
+ }) {
364
+ const [email, setEmail] = (0, import_react3.useState)("");
365
+ const [password, setPassword] = (0, import_react3.useState)("");
366
+ const [label, setLabel] = (0, import_react3.useState)("");
367
+ const [remember, setRemember] = (0, import_react3.useState)(true);
368
+ const [advanced, setAdvanced] = (0, import_react3.useState)(false);
369
+ const [imapHost, setImapHost] = (0, import_react3.useState)("");
370
+ const [imapPort, setImapPort] = (0, import_react3.useState)("993");
371
+ const [imapSecure, setImapSecure] = (0, import_react3.useState)(true);
372
+ const [smtpHost, setSmtpHost] = (0, import_react3.useState)("");
373
+ const [smtpPort, setSmtpPort] = (0, import_react3.useState)("465");
374
+ const [smtpSecure, setSmtpSecure] = (0, import_react3.useState)(true);
375
+ const [fromName, setFromName] = (0, import_react3.useState)("");
376
+ const [busy, setBusy] = (0, import_react3.useState)(false);
377
+ const [error, setError] = (0, import_react3.useState)("");
378
+ const submit = async () => {
379
+ setBusy(true);
380
+ setError("");
381
+ try {
382
+ const domain = domainOf(email);
383
+ const { state } = await api.addAccount({
384
+ email: email.trim(),
385
+ password,
386
+ remember,
387
+ ...label.trim() === "" ? {} : { label: label.trim() },
388
+ ...advanced ? {
389
+ imap: {
390
+ host: imapHost.trim() === "" ? `imap.${domain}` : imapHost.trim(),
391
+ port: Number(imapPort) || 993,
392
+ secure: imapSecure
393
+ },
394
+ smtp: {
395
+ host: smtpHost.trim() === "" ? `smtp.${domain}` : smtpHost.trim(),
396
+ port: Number(smtpPort) || 465,
397
+ secure: smtpSecure,
398
+ ...fromName.trim() === "" ? {} : { fromName: fromName.trim() }
399
+ }
400
+ } : {}
401
+ });
402
+ onDone(state);
403
+ } catch (caught) {
404
+ setError(errorText(caught));
405
+ } finally {
406
+ setBusy(false);
407
+ }
408
+ };
409
+ return (0, import_react3.createElement)(
410
+ "div",
411
+ { className: "mailharbor-card" },
412
+ (0, import_react3.createElement)("h3", null, "\u6DFB\u52A0\u90AE\u7BB1\u8D26\u53F7"),
413
+ (0, import_react3.createElement)(
414
+ "div",
415
+ { className: "mailharbor-grid" },
416
+ (0, import_react3.createElement)(
417
+ Field,
418
+ { label: "\u90AE\u7BB1\u5730\u5740" },
419
+ (0, import_react3.createElement)("input", {
420
+ type: "email",
421
+ value: email,
422
+ placeholder: "you@example.com",
423
+ onChange: (event) => setEmail(event.target.value),
424
+ autoFocus: true
425
+ })
426
+ ),
427
+ (0, import_react3.createElement)(
428
+ Field,
429
+ { label: "\u5BC6\u7801 / \u5E94\u7528\u4E13\u7528\u5BC6\u7801" },
430
+ (0, import_react3.createElement)("input", {
431
+ type: "password",
432
+ value: password,
433
+ onChange: (event) => setPassword(event.target.value)
434
+ })
435
+ )
436
+ ),
437
+ (0, import_react3.createElement)(
438
+ Field,
439
+ { label: "\u663E\u793A\u540D\u79F0\uFF08\u53EF\u9009\uFF09" },
440
+ (0, import_react3.createElement)("input", {
441
+ value: label,
442
+ placeholder: "\u5DE5\u4F5C\u90AE\u7BB1",
443
+ onChange: (event) => setLabel(event.target.value)
444
+ })
445
+ ),
446
+ (0, import_react3.createElement)(Toggle, { checked: remember, onChange: setRemember, label: "\u8BB0\u4F4F\u5BC6\u7801\uFF08\u5199\u5165\u7CFB\u7EDF\u94A5\u5319\u4E32\uFF09" }),
447
+ (0, import_react3.createElement)(
448
+ "button",
449
+ { className: "ghost", onClick: () => setAdvanced(!advanced) },
450
+ advanced ? "\u9690\u85CF\u9AD8\u7EA7\u8BBE\u7F6E" : "\u9AD8\u7EA7\u8BBE\u7F6E\uFF08\u670D\u52A1\u5668\u4E0E\u7AEF\u53E3\uFF09"
451
+ ),
452
+ advanced ? (0, import_react3.createElement)(
453
+ "div",
454
+ { style: { display: "flex", flexDirection: "column", gap: "10px" } },
455
+ (0, import_react3.createElement)(
456
+ "div",
457
+ { className: "mailharbor-grid three" },
458
+ (0, import_react3.createElement)(
459
+ Field,
460
+ { label: "IMAP \u4E3B\u673A" },
461
+ (0, import_react3.createElement)("input", {
462
+ value: imapHost,
463
+ placeholder: `imap.${domainOf(email)}`,
464
+ onChange: (event) => setImapHost(event.target.value)
465
+ })
466
+ ),
467
+ (0, import_react3.createElement)(
468
+ Field,
469
+ { label: "\u7AEF\u53E3" },
470
+ (0, import_react3.createElement)("input", {
471
+ value: imapPort,
472
+ inputMode: "numeric",
473
+ onChange: (event) => setImapPort(event.target.value)
474
+ })
475
+ ),
476
+ (0, import_react3.createElement)(Toggle, { checked: imapSecure, onChange: setImapSecure, label: "TLS" })
477
+ ),
478
+ (0, import_react3.createElement)(
479
+ "div",
480
+ { className: "mailharbor-grid three" },
481
+ (0, import_react3.createElement)(
482
+ Field,
483
+ { label: "SMTP \u4E3B\u673A" },
484
+ (0, import_react3.createElement)("input", {
485
+ value: smtpHost,
486
+ placeholder: `smtp.${domainOf(email)}`,
487
+ onChange: (event) => setSmtpHost(event.target.value)
488
+ })
489
+ ),
490
+ (0, import_react3.createElement)(
491
+ Field,
492
+ { label: "\u7AEF\u53E3" },
493
+ (0, import_react3.createElement)("input", {
494
+ value: smtpPort,
495
+ inputMode: "numeric",
496
+ onChange: (event) => setSmtpPort(event.target.value)
497
+ })
498
+ ),
499
+ (0, import_react3.createElement)(Toggle, { checked: smtpSecure, onChange: setSmtpSecure, label: "TLS" })
500
+ ),
501
+ (0, import_react3.createElement)(
502
+ Field,
503
+ { label: "\u53D1\u4EF6\u4EBA\u540D\u79F0\uFF08\u53EF\u9009\uFF09" },
504
+ (0, import_react3.createElement)("input", {
505
+ value: fromName,
506
+ onChange: (event) => setFromName(event.target.value)
507
+ })
508
+ )
509
+ ) : null,
510
+ error === "" ? null : (0, import_react3.createElement)("div", { className: "small", style: { color: "var(--m-danger)" } }, error),
511
+ (0, import_react3.createElement)(
512
+ "div",
513
+ { style: { display: "flex", gap: "8px", justifyContent: "flex-end" } },
514
+ onCancel === void 0 ? null : (0, import_react3.createElement)("button", { className: "ghost", onClick: onCancel }, "\u53D6\u6D88"),
515
+ (0, import_react3.createElement)(
516
+ "button",
517
+ {
518
+ className: "primary",
519
+ onClick: () => void submit(),
520
+ disabled: busy || email.trim() === "" || password === ""
521
+ },
522
+ busy ? "\u8FDE\u63A5\u4E2D\u2026" : "\u6DFB\u52A0\u5E76\u8FDE\u63A5"
523
+ )
524
+ )
525
+ );
526
+ }
527
+ function LoginForm({ account, onDone }) {
528
+ const [password, setPassword] = (0, import_react3.useState)("");
529
+ const [remember, setRemember] = (0, import_react3.useState)(true);
530
+ const [busy, setBusy] = (0, import_react3.useState)(false);
531
+ const [error, setError] = (0, import_react3.useState)("");
532
+ const submit = async () => {
533
+ setBusy(true);
534
+ setError("");
535
+ try {
536
+ await api.login(account.id, password, remember);
537
+ onDone();
538
+ } catch (caught) {
539
+ setError(errorText(caught));
540
+ } finally {
541
+ setBusy(false);
542
+ }
543
+ };
544
+ return (0, import_react3.createElement)(
545
+ "div",
546
+ { className: "mailharbor-card" },
547
+ (0, import_react3.createElement)("h3", null, `\u767B\u5F55 ${account.email}`),
548
+ (0, import_react3.createElement)(
549
+ "p",
550
+ { className: "small muted" },
551
+ `${account.imap.host}:${String(account.imap.port)} \xB7 ${account.smtp.host}:${String(account.smtp.port)}`
552
+ ),
553
+ (0, import_react3.createElement)(
554
+ Field,
555
+ { label: "\u5BC6\u7801 / \u5E94\u7528\u4E13\u7528\u5BC6\u7801" },
556
+ (0, import_react3.createElement)("input", {
557
+ type: "password",
558
+ value: password,
559
+ onChange: (event) => setPassword(event.target.value),
560
+ onKeyDown: (event) => {
561
+ if (event.key === "Enter") void submit();
562
+ },
563
+ autoFocus: true
564
+ })
565
+ ),
566
+ (0, import_react3.createElement)(Toggle, { checked: remember, onChange: setRemember, label: "\u8BB0\u4F4F\u5BC6\u7801\uFF08\u5199\u5165\u7CFB\u7EDF\u94A5\u5319\u4E32\uFF09" }),
567
+ error === "" ? null : (0, import_react3.createElement)("div", { className: "small", style: { color: "var(--m-danger)" } }, error),
568
+ (0, import_react3.createElement)(
569
+ "button",
570
+ { className: "primary", onClick: () => void submit(), disabled: busy || password === "" },
571
+ busy ? "\u767B\u5F55\u4E2D\u2026" : "\u767B\u5F55"
572
+ )
573
+ );
574
+ }
575
+ function AddAccountModal({
576
+ onClose,
577
+ onDone
578
+ }) {
579
+ return (0, import_react3.createElement)(
580
+ Modal,
581
+ { title: "\u6DFB\u52A0\u90AE\u7BB1\u8D26\u53F7", onClose },
582
+ (0, import_react3.createElement)(AddAccountForm, {
583
+ onDone: (state) => {
584
+ onDone(state);
585
+ onClose();
586
+ },
587
+ onCancel: onClose
588
+ })
589
+ );
590
+ }
591
+
592
+ // src/client/composer.tsx
593
+ var import_react4 = require("react");
594
+ function errorText2(error) {
595
+ return error instanceof ApiError || error instanceof Error ? error.message : String(error);
596
+ }
597
+ function readFileBase64(file) {
598
+ return new Promise((resolve, reject) => {
599
+ const reader = new FileReader();
600
+ reader.onload = () => {
601
+ const result = String(reader.result ?? "");
602
+ resolve(result.slice(result.indexOf(",") + 1));
603
+ };
604
+ reader.onerror = () => reject(new Error(`\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${file.name}`));
605
+ reader.readAsDataURL(file);
606
+ });
607
+ }
608
+ function Composer({
609
+ account,
610
+ initial,
611
+ onClose,
612
+ onSent
613
+ }) {
614
+ const [to, setTo] = (0, import_react4.useState)(initial.to);
615
+ const [cc, setCc] = (0, import_react4.useState)(initial.cc);
616
+ const [bcc, setBcc] = (0, import_react4.useState)(initial.bcc);
617
+ const [subject, setSubject] = (0, import_react4.useState)(initial.subject);
618
+ const [showCc, setShowCc] = (0, import_react4.useState)(initial.cc !== "" || initial.bcc !== "");
619
+ const [attachments, setAttachments] = (0, import_react4.useState)(initial.attachments ?? []);
620
+ const [busy, setBusy] = (0, import_react4.useState)(false);
621
+ const [error, setError] = (0, import_react4.useState)("");
622
+ const [confirming, setConfirming] = (0, import_react4.useState)(false);
623
+ const [draftId, setDraftId] = (0, import_react4.useState)(initial.draftId);
624
+ const editor = (0, import_react4.useRef)(null);
625
+ (0, import_react4.useEffect)(() => {
626
+ if (editor.current !== null) editor.current.innerHTML = initial.html;
627
+ }, []);
628
+ const html = () => editor.current?.innerHTML ?? "";
629
+ const text = () => editor.current?.innerText ?? "";
630
+ const exec = (command, value) => {
631
+ editor.current?.focus();
632
+ document.execCommand(command, false, value);
633
+ };
634
+ const addFiles = async (files) => {
635
+ if (files === null || files.length === 0) return;
636
+ setError("");
637
+ const next = [...attachments];
638
+ let total = next.reduce((sum, file) => sum + file.data.length, 0);
639
+ for (const file of Array.from(files)) {
640
+ if (total + file.size > 8 * 1024 * 1024) {
641
+ setError("\u9644\u4EF6\u5408\u8BA1\u4E0D\u80FD\u8D85\u8FC7 8 MB");
642
+ break;
643
+ }
644
+ const data = await readFileBase64(file);
645
+ next.push({ filename: file.name, contentType: file.type || "application/octet-stream", data });
646
+ total += file.size;
647
+ }
648
+ setAttachments(next);
649
+ };
650
+ const saveDraft = async () => {
651
+ setBusy(true);
652
+ setError("");
653
+ try {
654
+ const { draft } = await api.saveDraft(account.id, {
655
+ ...draftId === void 0 ? {} : { id: draftId },
656
+ to,
657
+ cc,
658
+ bcc,
659
+ subject,
660
+ html: html(),
661
+ attachments,
662
+ ...initial.inReplyTo === void 0 ? {} : { inReplyTo: initial.inReplyTo },
663
+ ...initial.references === void 0 ? {} : { references: initial.references }
664
+ });
665
+ setDraftId(draft.id);
666
+ onSent("\u8349\u7A3F\u5DF2\u4FDD\u5B58");
667
+ } catch (caught) {
668
+ setError(errorText2(caught));
669
+ } finally {
670
+ setBusy(false);
671
+ }
672
+ };
673
+ const send = async () => {
674
+ setBusy(true);
675
+ setError("");
676
+ try {
677
+ await api.send(account.id, {
678
+ to,
679
+ cc,
680
+ bcc,
681
+ subject,
682
+ html: html(),
683
+ text: text(),
684
+ attachments,
685
+ ...initial.inReplyTo === void 0 ? {} : { inReplyTo: initial.inReplyTo },
686
+ ...initial.references === void 0 ? {} : { references: initial.references }
687
+ });
688
+ if (draftId !== void 0) {
689
+ try {
690
+ await api.deleteDraft(draftId, account.id);
691
+ } catch {
692
+ }
693
+ }
694
+ onSent("\u90AE\u4EF6\u5DF2\u53D1\u9001");
695
+ onClose();
696
+ } catch (caught) {
697
+ setError(errorText2(caught));
698
+ } finally {
699
+ setBusy(false);
700
+ setConfirming(false);
701
+ }
702
+ };
703
+ return (0, import_react4.createElement)(
704
+ Modal,
705
+ {
706
+ title: initial.mode === "reply" ? "\u56DE\u590D" : initial.mode === "replyAll" ? "\u5168\u90E8\u56DE\u590D" : initial.mode === "forward" ? "\u8F6C\u53D1" : "\u5199\u90AE\u4EF6",
707
+ onClose,
708
+ wide: true,
709
+ footer: (0, import_react4.createElement)(
710
+ "div",
711
+ { style: { display: "flex", gap: "8px", alignItems: "center", width: "100%" } },
712
+ (0, import_react4.createElement)("span", { className: "small muted" }, `\u53D1\u4EF6\u4EBA ${account.email}`),
713
+ (0, import_react4.createElement)("span", { className: "spacer" }),
714
+ (0, import_react4.createElement)("button", { className: "ghost", onClick: () => void saveDraft(), disabled: busy }, "\u5B58\u8349\u7A3F"),
715
+ confirming ? (0, import_react4.createElement)(
716
+ "span",
717
+ { style: { display: "inline-flex", gap: "6px", alignItems: "center" } },
718
+ (0, import_react4.createElement)("span", { className: "small" }, "\u786E\u5B9A\u53D1\u9001\uFF1F"),
719
+ (0, import_react4.createElement)(
720
+ "button",
721
+ { className: "primary", onClick: () => void send(), disabled: busy },
722
+ busy ? "\u53D1\u9001\u4E2D\u2026" : "\u786E\u5B9A"
723
+ ),
724
+ (0, import_react4.createElement)("button", { className: "ghost", onClick: () => setConfirming(false) }, "\u53D6\u6D88")
725
+ ) : (0, import_react4.createElement)(
726
+ "button",
727
+ {
728
+ className: "primary",
729
+ onClick: () => setConfirming(true),
730
+ disabled: busy || to.trim() === ""
731
+ },
732
+ "\u53D1\u9001"
733
+ )
734
+ )
735
+ },
736
+ error === "" ? null : (0, import_react4.createElement)("div", { className: "small", style: { color: "var(--m-danger)" } }, error),
737
+ (0, import_react4.createElement)(
738
+ "div",
739
+ { className: "mailharbor-grid" },
740
+ (0, import_react4.createElement)(
741
+ Field,
742
+ { label: "\u6536\u4EF6\u4EBA" },
743
+ (0, import_react4.createElement)("input", {
744
+ value: to,
745
+ onChange: (event) => setTo(event.target.value),
746
+ placeholder: "a@example.com, b@example.com"
747
+ })
748
+ ),
749
+ (0, import_react4.createElement)(
750
+ Field,
751
+ { label: "\u4E3B\u9898" },
752
+ (0, import_react4.createElement)("input", {
753
+ value: subject,
754
+ onChange: (event) => setSubject(event.target.value)
755
+ })
756
+ )
757
+ ),
758
+ showCc ? (0, import_react4.createElement)(
759
+ "div",
760
+ { className: "mailharbor-grid" },
761
+ (0, import_react4.createElement)(
762
+ Field,
763
+ { label: "\u6284\u9001" },
764
+ (0, import_react4.createElement)("input", {
765
+ value: cc,
766
+ onChange: (event) => setCc(event.target.value)
767
+ })
768
+ ),
769
+ (0, import_react4.createElement)(
770
+ Field,
771
+ { label: "\u5BC6\u9001" },
772
+ (0, import_react4.createElement)("input", {
773
+ value: bcc,
774
+ onChange: (event) => setBcc(event.target.value)
775
+ })
776
+ )
777
+ ) : (0, import_react4.createElement)(
778
+ "button",
779
+ { className: "subtle small", style: { alignSelf: "flex-start" }, onClick: () => setShowCc(true) },
780
+ "\u6DFB\u52A0\u6284\u9001 / \u5BC6\u9001"
781
+ ),
782
+ (0, import_react4.createElement)(
783
+ "div",
784
+ { className: "mailharbor-toolbar" },
785
+ (0, import_react4.createElement)("button", { onClick: () => exec("bold"), title: "\u52A0\u7C97" }, "B"),
786
+ (0, import_react4.createElement)("button", { onClick: () => exec("italic"), title: "\u659C\u4F53" }, "I"),
787
+ (0, import_react4.createElement)("button", { onClick: () => exec("underline"), title: "\u4E0B\u5212\u7EBF" }, "U"),
788
+ (0, import_react4.createElement)("button", { onClick: () => exec("insertUnorderedList"), title: "\u65E0\u5E8F\u5217\u8868" }, "\u2022 \u5217\u8868"),
789
+ (0, import_react4.createElement)("button", { onClick: () => exec("insertOrderedList"), title: "\u6709\u5E8F\u5217\u8868" }, "1. \u5217\u8868"),
790
+ (0, import_react4.createElement)("button", { onClick: () => exec("formatBlock", "blockquote"), title: "\u5F15\u7528" }, "\u5F15\u7528"),
791
+ (0, import_react4.createElement)(
792
+ "button",
793
+ {
794
+ onClick: () => {
795
+ const url = window.prompt("\u94FE\u63A5\u5730\u5740\uFF08https://\u2026\uFF09");
796
+ if (url !== null && /^https?:\/\//i.test(url)) exec("createLink", url);
797
+ },
798
+ title: "\u94FE\u63A5"
799
+ },
800
+ "\u94FE\u63A5"
801
+ ),
802
+ (0, import_react4.createElement)("button", { onClick: () => exec("removeFormat"), title: "\u6E05\u9664\u683C\u5F0F" }, "\u6E05\u9664"),
803
+ (0, import_react4.createElement)("span", { className: "spacer" }),
804
+ (0, import_react4.createElement)(
805
+ "label",
806
+ { className: "mailharbor-toggle small" },
807
+ "\u9644\u4EF6",
808
+ (0, import_react4.createElement)("input", {
809
+ type: "file",
810
+ multiple: true,
811
+ style: { display: "none" },
812
+ onChange: (event) => {
813
+ void addFiles(event.target.files);
814
+ event.target.value = "";
815
+ }
816
+ })
817
+ )
818
+ ),
819
+ attachments.length === 0 ? null : (0, import_react4.createElement)(
820
+ "div",
821
+ { className: "mailharbor-attachments", style: { padding: 0, border: 0 } },
822
+ attachments.map(
823
+ (file, index) => (0, import_react4.createElement)(
824
+ "span",
825
+ { className: "mailharbor-attachment", key: `${file.filename}:${String(index)}` },
826
+ (0, import_react4.createElement)("span", { className: "name" }, file.filename),
827
+ (0, import_react4.createElement)(
828
+ "button",
829
+ {
830
+ className: "subtle",
831
+ onClick: () => setAttachments(attachments.filter((_item, position) => position !== index))
832
+ },
833
+ "\u2715"
834
+ )
835
+ )
836
+ )
837
+ ),
838
+ (0, import_react4.createElement)("div", {
839
+ className: "mailharbor-editor",
840
+ ref: editor,
841
+ contentEditable: true,
842
+ suppressContentEditableWarning: true,
843
+ "data-placeholder": "\u5199\u70B9\u4EC0\u4E48\u2026",
844
+ spellCheck: true
845
+ })
846
+ );
847
+ }
848
+
849
+ // src/client/icons.tsx
850
+ var import_react5 = require("react");
851
+ var PATHS = {
852
+ refresh: (0, import_react5.createElement)("path", { d: "M20 11a8 8 0 1 0-2.3 5.7M20 5v6h-6" }),
853
+ plus: (0, import_react5.createElement)("path", { d: "M12 5v14M5 12h14" }),
854
+ compose: (0, import_react5.createElement)("path", { d: "M4 20h4l10.5-10.5a2.1 2.1 0 0 0-3-3L5 17v3zM13.5 6.5l3 3" }),
855
+ archive: (0, import_react5.createElement)(
856
+ "g",
857
+ null,
858
+ (0, import_react5.createElement)("rect", { x: 3, y: 4, width: 18, height: 5, rx: 1.5 }),
859
+ (0, import_react5.createElement)("path", { d: "M5 9v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V9M10 13h4" })
860
+ ),
861
+ paperclip: (0, import_react5.createElement)("path", {
862
+ d: "M20 11.5 12 19.5a5 5 0 0 1-7-7l8-8a3.3 3.3 0 0 1 4.7 4.7l-8 8a1.7 1.7 0 0 1-2.4-2.4l7.3-7.3"
863
+ }),
864
+ mail: (0, import_react5.createElement)(
865
+ "g",
866
+ null,
867
+ (0, import_react5.createElement)("rect", { x: 3, y: 5, width: 18, height: 14, rx: 2.5 }),
868
+ (0, import_react5.createElement)("path", { d: "m4 7 7.1 5a1.6 1.6 0 0 0 1.8 0L20 7" })
869
+ ),
870
+ search: (0, import_react5.createElement)("path", { d: "M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14zM20 20l-4-4" }),
871
+ reply: (0, import_react5.createElement)("path", { d: "M9 7 4 12l5 5M4 12h9a6 6 0 0 1 6 6v1" }),
872
+ replyAll: (0, import_react5.createElement)("path", { d: "m9 7-5 5 5 5M14 7l-5 5 5 5M13 12h5a5 5 0 0 1 5 5" }),
873
+ forward: (0, import_react5.createElement)("path", { d: "m15 7 5 5-5 5M20 12h-9a6 6 0 0 0-6 6v1" }),
874
+ check: (0, import_react5.createElement)("path", { d: "m5 13 4 4L19 7" }),
875
+ image: (0, import_react5.createElement)(
876
+ "g",
877
+ null,
878
+ (0, import_react5.createElement)("rect", { x: 3, y: 4, width: 18, height: 16, rx: 2 }),
879
+ (0, import_react5.createElement)("circle", { cx: 8.5, cy: 9.5, r: 1.5 }),
880
+ (0, import_react5.createElement)("path", { d: "m4 18 5-5 4 4 3-3 4 4" })
881
+ ),
882
+ close: (0, import_react5.createElement)("path", { d: "M6 6l12 12M18 6 6 18" }),
883
+ chevronLeft: (0, import_react5.createElement)("path", { d: "m14 6-6 6 6 6" })
884
+ };
885
+ function Icon({ name, size = 16 }) {
886
+ return (0, import_react5.createElement)(
887
+ "svg",
888
+ {
889
+ width: size,
890
+ height: size,
891
+ viewBox: "0 0 24 24",
892
+ fill: "none",
893
+ stroke: "currentColor",
894
+ strokeWidth: 1.7,
895
+ strokeLinecap: "round",
896
+ strokeLinejoin: "round",
897
+ "aria-hidden": "true",
898
+ focusable: "false"
899
+ },
900
+ PATHS[name] ?? null
901
+ );
902
+ }
903
+
904
+ // src/client/reader.tsx
905
+ var import_react6 = require("react");
906
+ var IFRAME_STYLE = `
907
+ html, body { margin: 0; padding: 0; }
908
+ body {
909
+ font: 15px/1.75 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
910
+ color: #171a21; background: #fff; padding: 20px 22px;
911
+ overflow-wrap: anywhere;
912
+ }
913
+ img { max-width: 100%; height: auto; }
914
+ table { max-width: 100%; border-collapse: collapse; }
915
+ td, th { padding: 2px 6px; }
916
+ a { color: #2f5bd3; }
917
+ blockquote { margin: 0 0 0 12px; padding-left: 12px; border-left: 2px solid #d7dbe3; color: #565c67; }
918
+ pre { white-space: pre-wrap; }
919
+ `;
920
+ function csp(allowRemoteImages) {
921
+ return [
922
+ "default-src 'none'",
923
+ "style-src 'unsafe-inline'",
924
+ "font-src data:",
925
+ `img-src data:${allowRemoteImages ? " https: http:" : ""}`,
926
+ "form-action 'none'",
927
+ "base-uri 'none'"
928
+ ].join("; ");
929
+ }
930
+ function srcDoc(detail, allowRemoteImages) {
931
+ const html = detail.html !== null && detail.html !== "" ? detail.html : `<pre>${escapeHtml(detail.text)}</pre>`;
932
+ return `<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="${csp(allowRemoteImages)}"><style>${IFRAME_STYLE}</style></head><body>${html}</body></html>`;
933
+ }
934
+ function escapeHtml(value) {
935
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
936
+ }
937
+ function hasRemoteImages(detail) {
938
+ return detail.html !== null && /<img[^>]+src=["']https?:/i.test(detail.html);
939
+ }
940
+ function addressLine(list) {
941
+ return list.map((item) => item.name !== "" ? `${item.name} <${item.address}>` : item.address).join("\u3001");
942
+ }
943
+ function Reader({
944
+ detail,
945
+ account,
946
+ source,
947
+ mailbox,
948
+ onChanged,
949
+ onReply,
950
+ onClose
951
+ }) {
952
+ const [plain, setPlain] = (0, import_react6.useState)(false);
953
+ const [loadImages, setLoadImages] = (0, import_react6.useState)(false);
954
+ const [busy, setBusy] = (0, import_react6.useState)(false);
955
+ const [error, setError] = (0, import_react6.useState)("");
956
+ const remoteImages = (0, import_react6.useMemo)(() => hasRemoteImages(detail), [detail]);
957
+ const html = detail.html !== null && detail.html !== "";
958
+ const toggleRead = async () => {
959
+ setBusy(true);
960
+ setError("");
961
+ try {
962
+ const result = await api.setRead({
963
+ id: detail.id,
964
+ accountId: account.id,
965
+ source,
966
+ mailbox,
967
+ read: detail.unread
968
+ });
969
+ onChanged({ unread: !result.read });
970
+ } catch (caught) {
971
+ setError(caught instanceof Error ? caught.message : String(caught));
972
+ } finally {
973
+ setBusy(false);
974
+ }
975
+ };
976
+ return (0, import_react6.createElement)(
977
+ "div",
978
+ { className: "mailharbor-reader" },
979
+ (0, import_react6.createElement)(
980
+ "div",
981
+ { className: "mailharbor-reader-head" },
982
+ (0, import_react6.createElement)(
983
+ "div",
984
+ { className: "mailharbor-reader-title" },
985
+ onClose === void 0 ? null : (0, import_react6.createElement)(
986
+ "button",
987
+ { className: "ghost icon", onClick: onClose, "aria-label": "\u8FD4\u56DE\u5217\u8868", title: "\u8FD4\u56DE\u5217\u8868" },
988
+ (0, import_react6.createElement)(Icon, { name: "chevronLeft" })
989
+ ),
990
+ (0, import_react6.createElement)("h2", { style: { flex: 1 } }, detail.subject),
991
+ (0, import_react6.createElement)(
992
+ "button",
993
+ {
994
+ className: "ghost icon",
995
+ onClick: () => void toggleRead(),
996
+ disabled: busy,
997
+ title: detail.unread ? "\u6807\u4E3A\u5DF2\u8BFB" : "\u6807\u4E3A\u672A\u8BFB",
998
+ "aria-label": detail.unread ? "\u6807\u4E3A\u5DF2\u8BFB" : "\u6807\u4E3A\u672A\u8BFB"
999
+ },
1000
+ (0, import_react6.createElement)(Icon, { name: "check" })
1001
+ )
1002
+ ),
1003
+ (0, import_react6.createElement)(
1004
+ "div",
1005
+ { className: "mailharbor-reader-meta" },
1006
+ (0, import_react6.createElement)(
1007
+ "span",
1008
+ { className: "who" },
1009
+ (0, import_react6.createElement)(Avatar, { email: detail.from.address, size: 22 }),
1010
+ (0, import_react6.createElement)("strong", null, detail.from.name !== "" ? detail.from.name : detail.from.address),
1011
+ detail.from.name !== "" ? (0, import_react6.createElement)("span", null, `<${detail.from.address}>`) : null
1012
+ ),
1013
+ (0, import_react6.createElement)("span", null, longDate(detail.date)),
1014
+ detail.backedUp === false ? (0, import_react6.createElement)("span", { className: "badge warn" }, "\u672A\u5907\u4EFD") : null
1015
+ ),
1016
+ (0, import_react6.createElement)(
1017
+ "div",
1018
+ { className: "mailharbor-reader-meta" },
1019
+ (0, import_react6.createElement)("span", null, `\u6536\u4EF6\u4EBA: ${addressLine(detail.to) || "\u2014"}`),
1020
+ detail.cc.length === 0 ? null : (0, import_react6.createElement)("span", null, `\u6284\u9001: ${addressLine(detail.cc)}`)
1021
+ ),
1022
+ (0, import_react6.createElement)(
1023
+ "div",
1024
+ { className: "mailharbor-reader-actions" },
1025
+ (0, import_react6.createElement)(
1026
+ "button",
1027
+ { className: "ghost", onClick: () => onReply("reply") },
1028
+ (0, import_react6.createElement)(Icon, { name: "reply" }),
1029
+ "\u56DE\u590D"
1030
+ ),
1031
+ (0, import_react6.createElement)(
1032
+ "button",
1033
+ { className: "ghost", onClick: () => onReply("replyAll") },
1034
+ (0, import_react6.createElement)(Icon, { name: "replyAll" }),
1035
+ "\u5168\u90E8\u56DE\u590D"
1036
+ ),
1037
+ (0, import_react6.createElement)(
1038
+ "button",
1039
+ { className: "ghost", onClick: () => onReply("forward") },
1040
+ (0, import_react6.createElement)(Icon, { name: "forward" }),
1041
+ "\u8F6C\u53D1"
1042
+ ),
1043
+ (0, import_react6.createElement)("span", { className: "spacer" }),
1044
+ html ? (0, import_react6.createElement)(
1045
+ "button",
1046
+ { className: "subtle", onClick: () => setPlain(!plain) },
1047
+ plain ? "\u663E\u793A HTML" : "\u7EAF\u6587\u672C"
1048
+ ) : null,
1049
+ remoteImages && !loadImages ? (0, import_react6.createElement)(
1050
+ "button",
1051
+ { className: "subtle", onClick: () => setLoadImages(true) },
1052
+ (0, import_react6.createElement)(Icon, { name: "image", size: 14 }),
1053
+ "\u663E\u793A\u56FE\u7247"
1054
+ ) : null
1055
+ ),
1056
+ error === "" ? null : (0, import_react6.createElement)("div", { className: "small", style: { color: "var(--m-danger)" } }, error)
1057
+ ),
1058
+ (0, import_react6.createElement)(
1059
+ "div",
1060
+ { className: "mailharbor-reader-body" },
1061
+ html && !plain ? (0, import_react6.createElement)("iframe", {
1062
+ title: "\u90AE\u4EF6\u6B63\u6587",
1063
+ sandbox: "",
1064
+ srcDoc: srcDoc(detail, loadImages),
1065
+ referrerPolicy: "no-referrer"
1066
+ }) : (0, import_react6.createElement)("pre", null, detail.text !== "" ? detail.text : "\uFF08\u65E0\u6B63\u6587\uFF09")
1067
+ ),
1068
+ detail.attachments.length === 0 ? null : (0, import_react6.createElement)(
1069
+ "div",
1070
+ { className: "mailharbor-attachments" },
1071
+ detail.attachments.map(
1072
+ (file) => (0, import_react6.createElement)(
1073
+ "a",
1074
+ {
1075
+ className: "mailharbor-attachment",
1076
+ key: file.id,
1077
+ href: source === "server" ? api.attachmentUrl({
1078
+ accountId: account.id,
1079
+ source: "server",
1080
+ mailbox,
1081
+ uid: detail.uid ?? "",
1082
+ part: file.id
1083
+ }) : api.attachmentUrl({
1084
+ accountId: account.id,
1085
+ source: "local",
1086
+ emailId: detail.id,
1087
+ attachmentId: file.id
1088
+ }),
1089
+ download: file.filename,
1090
+ title: file.filename
1091
+ },
1092
+ (0, import_react6.createElement)(Icon, { name: "paperclip", size: 14 }),
1093
+ (0, import_react6.createElement)("span", { className: "name" }, file.filename),
1094
+ (0, import_react6.createElement)("span", { className: "muted small" }, formatBytes(file.size))
1095
+ )
1096
+ )
1097
+ )
1098
+ );
1099
+ }
1100
+
1101
+ // src/client/panel.tsx
1102
+ var EMPTY_PAGE = { items: [], total: 0, offset: 0, limit: 40 };
1103
+ function errorText3(error) {
1104
+ return error instanceof ApiError || error instanceof Error ? error.message : String(error);
1105
+ }
1106
+ function matchesNarrow() {
1107
+ return typeof window.matchMedia === "function" ? window.matchMedia("(max-width: 900px)").matches : false;
1108
+ }
1109
+ function quoteForReply(detail) {
1110
+ const body = detail.html ?? `<pre>${detail.text.replaceAll("&", "&amp;").replaceAll("<", "&lt;")}</pre>`;
1111
+ return `<p><br></p><p>------------------ \u539F\u59CB\u90AE\u4EF6 ------------------</p><blockquote style="margin:0 0 0 12px;padding-left:12px;border-left:2px solid #d7dbe3;color:#565c67">${body}</blockquote>`;
1112
+ }
1113
+ function MailPanel() {
1114
+ const [state, setState] = (0, import_react7.useState)(null);
1115
+ const [error, setError] = (0, import_react7.useState)("");
1116
+ const [source, setSource] = (0, import_react7.useState)("local");
1117
+ const [mailbox, setMailbox] = (0, import_react7.useState)("all");
1118
+ const [folders, setFolders] = (0, import_react7.useState)([]);
1119
+ const [query2, setQuery] = (0, import_react7.useState)("");
1120
+ const [search, setSearch] = (0, import_react7.useState)("");
1121
+ const [page, setPage] = (0, import_react7.useState)(EMPTY_PAGE);
1122
+ const [limit, setLimit] = (0, import_react7.useState)(40);
1123
+ const [loading, setLoading] = (0, import_react7.useState)(false);
1124
+ const [selected, setSelected] = (0, import_react7.useState)(null);
1125
+ const [compose, setCompose] = (0, import_react7.useState)(null);
1126
+ const [showAdd, setShowAdd] = (0, import_react7.useState)(false);
1127
+ const [showBackup, setShowBackup] = (0, import_react7.useState)(false);
1128
+ const [backup, setBackup] = (0, import_react7.useState)(null);
1129
+ const [toast, setToast] = (0, import_react7.useState)("");
1130
+ const [narrow, setNarrow] = (0, import_react7.useState)(() => matchesNarrow());
1131
+ const [tick, setTick] = (0, import_react7.useState)(0);
1132
+ const account = state?.accounts.find((item) => item.id === state.activeAccountId) ?? null;
1133
+ const accountId = account?.id ?? null;
1134
+ const refresh = (0, import_react7.useCallback)(() => {
1135
+ setTick((value) => value + 1);
1136
+ }, []);
1137
+ const notice = (0, import_react7.useCallback)((message) => {
1138
+ setToast(message);
1139
+ window.setTimeout(() => {
1140
+ setToast("");
1141
+ }, 3200);
1142
+ }, []);
1143
+ (0, import_react7.useEffect)(() => {
1144
+ let cancelled = false;
1145
+ api.state().then((next) => {
1146
+ if (!cancelled) {
1147
+ setState(next);
1148
+ if (next.settings.render.listLimit > 0) setLimit(next.settings.render.listLimit);
1149
+ }
1150
+ }).catch((caught) => {
1151
+ if (!cancelled) setError(errorText3(caught));
1152
+ });
1153
+ return () => {
1154
+ cancelled = true;
1155
+ };
1156
+ }, [tick]);
1157
+ (0, import_react7.useEffect)(() => {
1158
+ if (accountId === null) return;
1159
+ let cancelled = false;
1160
+ api.folders({ accountId, source }).then((result) => {
1161
+ if (cancelled) return;
1162
+ const items2 = source === "local" ? [
1163
+ {
1164
+ name: "all",
1165
+ label: "\u5168\u90E8\u90AE\u4EF6",
1166
+ count: result.items.reduce((sum, item) => sum + item.count, 0)
1167
+ },
1168
+ ...result.items.map((item) => ({ ...item, label: item.label ?? item.name }))
1169
+ ] : result.items.map((item) => ({ ...item, label: item.label ?? item.name }));
1170
+ setFolders(items2);
1171
+ }).catch(() => {
1172
+ if (!cancelled) setFolders([]);
1173
+ });
1174
+ return () => {
1175
+ cancelled = true;
1176
+ };
1177
+ }, [accountId, source, tick]);
1178
+ (0, import_react7.useEffect)(() => {
1179
+ setMailbox(source === "local" ? "all" : "INBOX");
1180
+ setSelected(null);
1181
+ }, [source]);
1182
+ (0, import_react7.useEffect)(() => {
1183
+ if (accountId === null) return;
1184
+ let cancelled = false;
1185
+ setLoading(true);
1186
+ api.messages({ accountId, source, mailbox, query: search, limit }).then((result) => {
1187
+ if (!cancelled) setPage(result);
1188
+ }).catch((caught) => {
1189
+ if (!cancelled) setError(errorText3(caught));
1190
+ }).finally(() => {
1191
+ if (!cancelled) setLoading(false);
1192
+ });
1193
+ return () => {
1194
+ cancelled = true;
1195
+ };
1196
+ }, [accountId, source, mailbox, search, limit, tick]);
1197
+ (0, import_react7.useEffect)(() => {
1198
+ if (typeof window.matchMedia !== "function") return;
1199
+ const media = window.matchMedia("(max-width: 900px)");
1200
+ const onChange = () => setNarrow(media.matches);
1201
+ media.addEventListener("change", onChange);
1202
+ return () => {
1203
+ media.removeEventListener("change", onChange);
1204
+ };
1205
+ }, []);
1206
+ (0, import_react7.useEffect)(() => {
1207
+ if (!showBackup || accountId === null) return;
1208
+ let cancelled = false;
1209
+ const poll = async () => {
1210
+ try {
1211
+ const status = await api.backupStatus(accountId);
1212
+ if (!cancelled) setBackup(status);
1213
+ } catch {
1214
+ }
1215
+ };
1216
+ void poll();
1217
+ const timer = window.setInterval(() => {
1218
+ void poll();
1219
+ }, 1e3);
1220
+ return () => {
1221
+ cancelled = true;
1222
+ window.clearInterval(timer);
1223
+ };
1224
+ }, [showBackup, accountId]);
1225
+ const openMessage = async (item) => {
1226
+ if (account === null) return;
1227
+ setError("");
1228
+ try {
1229
+ const detail = await api.message({ id: item.id, accountId: account.id, source, mailbox });
1230
+ setSelected(detail);
1231
+ if (detail.unread && state?.settings.read.markOnOpen === true) {
1232
+ try {
1233
+ await api.setRead({ id: detail.id, accountId: account.id, source, mailbox, read: true });
1234
+ setSelected({ ...detail, unread: false });
1235
+ setPage((current) => ({
1236
+ ...current,
1237
+ items: current.items.map((row) => row.id === item.id ? { ...row, unread: false } : row)
1238
+ }));
1239
+ } catch {
1240
+ }
1241
+ }
1242
+ } catch (caught) {
1243
+ setError(errorText3(caught));
1244
+ }
1245
+ };
1246
+ const startCompose = (mode) => {
1247
+ if (account === null) return;
1248
+ const detail = selected;
1249
+ if (mode === "new" || detail === null) {
1250
+ setCompose({ mode: "new", to: "", cc: "", bcc: "", subject: "", html: "" });
1251
+ return;
1252
+ }
1253
+ const subjectPrefix = mode === "forward" ? "Fwd: " : "Re: ";
1254
+ const subject = /^(re|fwd):/i.test(detail.subject) ? detail.subject : `${subjectPrefix}${detail.subject}`;
1255
+ if (mode === "forward") {
1256
+ setCompose({
1257
+ mode,
1258
+ to: "",
1259
+ cc: "",
1260
+ bcc: "",
1261
+ subject,
1262
+ html: quoteForReply(detail),
1263
+ references: detail.references.join(" ")
1264
+ });
1265
+ return;
1266
+ }
1267
+ const to = detail.from.address;
1268
+ const cc = mode === "replyAll" ? [...detail.to, ...detail.cc].map((item) => item.address).filter((address) => address !== account.email && address !== to).join(", ") : "";
1269
+ setCompose({
1270
+ mode,
1271
+ to,
1272
+ cc,
1273
+ bcc: "",
1274
+ subject,
1275
+ html: quoteForReply(detail),
1276
+ ...detail.messageId === "" ? {} : { inReplyTo: detail.messageId, references: [...detail.references, detail.messageId].join(" ") }
1277
+ });
1278
+ };
1279
+ const switchAccount = async (id) => {
1280
+ try {
1281
+ await api.activateAccount(id);
1282
+ setSelected(null);
1283
+ refresh();
1284
+ } catch (caught) {
1285
+ setError(errorText3(caught));
1286
+ }
1287
+ };
1288
+ const startBackup = async () => {
1289
+ if (accountId === null) return;
1290
+ try {
1291
+ const result = await api.startBackup(accountId);
1292
+ setBackup(result.status);
1293
+ setShowBackup(true);
1294
+ } catch (caught) {
1295
+ setError(errorText3(caught));
1296
+ }
1297
+ };
1298
+ const stopBackup = async () => {
1299
+ if (accountId === null) return;
1300
+ try {
1301
+ const result = await api.stopBackup(accountId);
1302
+ setBackup(result.status);
1303
+ } catch (caught) {
1304
+ setError(errorText3(caught));
1305
+ }
1306
+ };
1307
+ const folderOptions = (0, import_react7.useMemo)(
1308
+ () => folders.map((folder) => ({
1309
+ value: folder.name,
1310
+ label: `${folder.label ?? folder.name}${folder.count > 0 ? ` (${String(folder.count)})` : ""}`
1311
+ })),
1312
+ [folders]
1313
+ );
1314
+ if (state === null) {
1315
+ return (0, import_react7.createElement)(
1316
+ "div",
1317
+ { className: "mailharbor" },
1318
+ (0, import_react7.createElement)("div", { className: "mailharbor-empty" }, error === "" ? "\u6B63\u5728\u52A0\u8F7D\u90AE\u7BB1\u2026" : error)
1319
+ );
1320
+ }
1321
+ if (account === null) {
1322
+ return (0, import_react7.createElement)(
1323
+ "div",
1324
+ { className: "mailharbor" },
1325
+ (0, import_react7.createElement)(
1326
+ "div",
1327
+ { className: "mailharbor-settings" },
1328
+ (0, import_react7.createElement)("h2", null, "\u6B22\u8FCE\u4F7F\u7528 MailHarbor"),
1329
+ (0, import_react7.createElement)(
1330
+ "p",
1331
+ { className: "muted small" },
1332
+ "\u628A\u4EFB\u610F IMAP/SMTP \u90AE\u7BB1\u63A5\u8FDB DeepSeek Harness\uFF1A\u672C\u5730\u5907\u4EFD\u3001\u5B9E\u65F6\u8BFB\u4FE1\u3001\u5199\u90AE\u4EF6\u4E0E Agent \u5DE5\u5177\u3002"
1333
+ ),
1334
+ error === "" ? null : (0, import_react7.createElement)("div", { className: "small", style: { color: "var(--m-danger)" } }, error),
1335
+ (0, import_react7.createElement)(AddAccountForm, {
1336
+ onDone: (next) => {
1337
+ setState(next);
1338
+ setSource("local");
1339
+ }
1340
+ })
1341
+ )
1342
+ );
1343
+ }
1344
+ const needsLogin = source === "server" && state.loggedIn === false;
1345
+ const items = page.items;
1346
+ return (0, import_react7.createElement)(
1347
+ "div",
1348
+ { className: narrow ? "mailharbor mailharbor-narrow" : "mailharbor" },
1349
+ (0, import_react7.createElement)(
1350
+ "div",
1351
+ { className: "mailharbor-topbar" },
1352
+ (0, import_react7.createElement)(
1353
+ "div",
1354
+ { className: "mailharbor-account" },
1355
+ (0, import_react7.createElement)(Avatar, { email: account.email, size: 26 }),
1356
+ state.accounts.length > 1 ? (0, import_react7.createElement)(
1357
+ "select",
1358
+ {
1359
+ value: account.id,
1360
+ onChange: (event) => void switchAccount(event.target.value),
1361
+ "aria-label": "\u8D26\u53F7"
1362
+ },
1363
+ state.accounts.map((item) => (0, import_react7.createElement)("option", { key: item.id, value: item.id }, item.label))
1364
+ ) : (0, import_react7.createElement)("span", { className: "name" }, account.label),
1365
+ state.loggedIn === false ? (0, import_react7.createElement)("span", { className: "badge warn" }, "\u672A\u767B\u5F55") : null
1366
+ ),
1367
+ (0, import_react7.createElement)(
1368
+ "div",
1369
+ { className: "mailharbor-segment", role: "tablist" },
1370
+ (0, import_react7.createElement)(
1371
+ "button",
1372
+ { className: source === "local" ? "active" : "", onClick: () => setSource("local"), role: "tab" },
1373
+ "\u672C\u5730\u5907\u4EFD"
1374
+ ),
1375
+ (0, import_react7.createElement)(
1376
+ "button",
1377
+ { className: source === "server" ? "active" : "", onClick: () => setSource("server"), role: "tab" },
1378
+ "\u670D\u52A1\u5668\u5B9E\u65F6"
1379
+ )
1380
+ ),
1381
+ (0, import_react7.createElement)("span", { className: "spacer" }),
1382
+ (0, import_react7.createElement)(
1383
+ "button",
1384
+ {
1385
+ className: "ghost icon",
1386
+ title: "\u6DFB\u52A0\u8D26\u53F7",
1387
+ "aria-label": "\u6DFB\u52A0\u8D26\u53F7",
1388
+ onClick: () => setShowAdd(true)
1389
+ },
1390
+ (0, import_react7.createElement)(Icon, { name: "plus" })
1391
+ ),
1392
+ (0, import_react7.createElement)(
1393
+ "button",
1394
+ { className: "ghost icon", title: "\u5237\u65B0\u5217\u8868", "aria-label": "\u5237\u65B0\u5217\u8868", onClick: refresh },
1395
+ (0, import_react7.createElement)(Icon, { name: "refresh" })
1396
+ ),
1397
+ (0, import_react7.createElement)(
1398
+ "button",
1399
+ { className: "ghost", onClick: () => void startBackup() },
1400
+ (0, import_react7.createElement)(Icon, { name: "archive" }),
1401
+ "\u5907\u4EFD"
1402
+ ),
1403
+ (0, import_react7.createElement)(
1404
+ "button",
1405
+ { className: "primary", onClick: () => startCompose("new") },
1406
+ (0, import_react7.createElement)(Icon, { name: "compose" }),
1407
+ "\u5199\u90AE\u4EF6"
1408
+ )
1409
+ ),
1410
+ toast === "" ? null : (0, import_react7.createElement)("div", { className: "small", style: { padding: "6px 14px", color: "var(--m-ok)" } }, toast),
1411
+ error === "" ? null : (0, import_react7.createElement)(ErrorBar, { message: error, onDismiss: () => setError("") }),
1412
+ needsLogin ? (0, import_react7.createElement)("div", { className: "mailharbor-settings" }, (0, import_react7.createElement)(LoginForm, { account, onDone: refresh })) : (0, import_react7.createElement)(
1413
+ "div",
1414
+ { className: "mailharbor-body", "data-mode": narrow && selected !== null ? "reader" : "list" },
1415
+ (0, import_react7.createElement)(
1416
+ "div",
1417
+ { className: "mailharbor-list" },
1418
+ (0, import_react7.createElement)(
1419
+ "div",
1420
+ { className: "mailharbor-filters" },
1421
+ (0, import_react7.createElement)(
1422
+ "select",
1423
+ {
1424
+ value: mailbox,
1425
+ onChange: (event) => {
1426
+ setMailbox(event.target.value);
1427
+ setSelected(null);
1428
+ },
1429
+ "aria-label": "\u90AE\u7BB1\u6587\u4EF6\u5939"
1430
+ },
1431
+ folderOptions.map(
1432
+ (option) => (0, import_react7.createElement)("option", { key: option.value, value: option.value }, option.label)
1433
+ )
1434
+ ),
1435
+ (0, import_react7.createElement)("input", {
1436
+ type: "search",
1437
+ placeholder: "\u641C\u7D22\u90AE\u4EF6",
1438
+ "aria-label": "\u641C\u7D22\u90AE\u4EF6",
1439
+ value: query2,
1440
+ onChange: (event) => setQuery(event.target.value),
1441
+ onKeyDown: (event) => {
1442
+ if (event.key === "Enter") setSearch(query2);
1443
+ if (event.key === "Escape") {
1444
+ setQuery("");
1445
+ setSearch("");
1446
+ }
1447
+ }
1448
+ }),
1449
+ query2 !== search || search !== "" ? (0, import_react7.createElement)(
1450
+ "button",
1451
+ {
1452
+ className: "subtle",
1453
+ title: "\u6E05\u9664\u641C\u7D22",
1454
+ onClick: () => {
1455
+ setQuery("");
1456
+ setSearch("");
1457
+ }
1458
+ },
1459
+ (0, import_react7.createElement)(Icon, { name: "close", size: 14 })
1460
+ ) : null
1461
+ ),
1462
+ (0, import_react7.createElement)(
1463
+ "div",
1464
+ { className: "mailharbor-rows" },
1465
+ loading && items.length === 0 ? (0, import_react7.createElement)("div", { className: "mailharbor-empty" }, "\u6B63\u5728\u8BFB\u53D6\u2026") : items.length === 0 ? (0, import_react7.createElement)(
1466
+ "div",
1467
+ { className: "mailharbor-empty" },
1468
+ (0, import_react7.createElement)(Icon, { name: "mail", size: 28 }),
1469
+ (0, import_react7.createElement)(
1470
+ "div",
1471
+ { className: "title" },
1472
+ source === "local" ? "\u672C\u5730\u5907\u4EFD\u91CC\u8FD8\u6CA1\u6709\u90AE\u4EF6" : "\u670D\u52A1\u5668\u4E0A\u6CA1\u6709\u90AE\u4EF6"
1473
+ ),
1474
+ (0, import_react7.createElement)(
1475
+ "div",
1476
+ { className: "small" },
1477
+ source === "local" ? "\u70B9\u9876\u90E8\u300C\u5907\u4EFD\u300D\u628A\u670D\u52A1\u5668\u90AE\u4EF6\u540C\u6B65\u4E0B\u6765\u3002" : "\u6362\u4E00\u4E2A\u6587\u4EF6\u5939\u6216\u7A0D\u540E\u5237\u65B0\u3002"
1478
+ )
1479
+ ) : items.map(
1480
+ (item) => (0, import_react7.createElement)(
1481
+ "div",
1482
+ {
1483
+ key: item.id,
1484
+ className: `mailharbor-row${item.unread ? " unread" : ""}${selected?.id === item.id ? " selected" : ""}`,
1485
+ role: "button",
1486
+ tabIndex: 0,
1487
+ onClick: () => void openMessage(item),
1488
+ onKeyDown: (event) => {
1489
+ if (event.key === "Enter" || event.key === " ") {
1490
+ event.preventDefault();
1491
+ void openMessage(item);
1492
+ }
1493
+ }
1494
+ },
1495
+ (0, import_react7.createElement)(
1496
+ "span",
1497
+ { className: "line-1" },
1498
+ item.unread ? (0, import_react7.createElement)("span", { className: "dot" }) : null,
1499
+ (0, import_react7.createElement)(
1500
+ "span",
1501
+ { className: "from" },
1502
+ item.from.name !== "" ? item.from.name : item.from.address
1503
+ ),
1504
+ item.hasAttachments ? (0, import_react7.createElement)(Icon, { name: "paperclip", size: 13 }) : null,
1505
+ item.backedUp === false ? (0, import_react7.createElement)("span", { className: "badge warn" }, "\u672A\u5907\u4EFD") : null,
1506
+ (0, import_react7.createElement)("span", { className: "date" }, shortDate(item.date))
1507
+ ),
1508
+ (0, import_react7.createElement)("span", { className: "line-2" }, (0, import_react7.createElement)("span", { className: "subject" }, item.subject)),
1509
+ item.snippet === "" ? null : (0, import_react7.createElement)("span", { className: "snippet" }, item.snippet)
1510
+ )
1511
+ )
1512
+ ),
1513
+ items.length > 0 ? (0, import_react7.createElement)(
1514
+ "div",
1515
+ { className: "mailharbor-list-foot" },
1516
+ (0, import_react7.createElement)("span", null, `${String(items.length)} / ${String(page.total)} \u5C01`),
1517
+ (0, import_react7.createElement)("span", { className: "spacer" }),
1518
+ page.total > items.length ? (0, import_react7.createElement)(
1519
+ "button",
1520
+ { className: "subtle", onClick: () => setLimit((current) => current + 40) },
1521
+ "\u52A0\u8F7D\u66F4\u591A"
1522
+ ) : null
1523
+ ) : null
1524
+ ),
1525
+ (0, import_react7.createElement)(
1526
+ "div",
1527
+ { className: "mailharbor-reader" },
1528
+ selected === null ? (0, import_react7.createElement)(
1529
+ "div",
1530
+ { className: "mailharbor-empty" },
1531
+ (0, import_react7.createElement)(Icon, { name: "mail", size: 34 }),
1532
+ (0, import_react7.createElement)("div", { className: "title" }, "\u9009\u62E9\u4E00\u5C01\u90AE\u4EF6\u5F00\u59CB\u9605\u8BFB"),
1533
+ (0, import_react7.createElement)(
1534
+ "div",
1535
+ { className: "small" },
1536
+ state.loggedIn ? "\u5DF2\u767B\u5F55\uFF0C\u53EF\u5207\u6362\u300C\u670D\u52A1\u5668\u5B9E\u65F6\u300D\u67E5\u770B\u6700\u65B0\u90AE\u4EF6\u3002" : "\u6D4F\u89C8\u672C\u5730\u5907\u4EFD\u4E0D\u9700\u8981\u767B\u5F55\u3002"
1537
+ )
1538
+ ) : (0, import_react7.createElement)(Reader, {
1539
+ detail: selected,
1540
+ account,
1541
+ source,
1542
+ mailbox,
1543
+ onChanged: (patch) => setSelected({ ...selected, ...patch }),
1544
+ onReply: (mode) => startCompose(mode),
1545
+ onClose: narrow ? () => setSelected(null) : void 0
1546
+ })
1547
+ )
1548
+ ),
1549
+ showBackup && backup !== null ? (0, import_react7.createElement)(
1550
+ Modal,
1551
+ {
1552
+ title: "\u5907\u4EFD",
1553
+ onClose: () => setShowBackup(false),
1554
+ footer: (0, import_react7.createElement)(
1555
+ "div",
1556
+ { style: { display: "flex", gap: "8px", width: "100%", alignItems: "center" } },
1557
+ (0, import_react7.createElement)("span", { className: "small muted" }, backup.running ? "\u5907\u4EFD\u8FDB\u884C\u4E2D\u2026" : backup.message),
1558
+ (0, import_react7.createElement)("span", { className: "spacer" }),
1559
+ backup.running ? (0, import_react7.createElement)("button", { className: "ghost danger", onClick: () => void stopBackup() }, "\u505C\u6B62\u5907\u4EFD") : (0, import_react7.createElement)("button", { className: "primary", onClick: () => void startBackup() }, "\u5F00\u59CB\u5907\u4EFD"),
1560
+ (0, import_react7.createElement)("button", { className: "ghost", onClick: () => setShowBackup(false) }, "\u5173\u95ED")
1561
+ )
1562
+ },
1563
+ (0, import_react7.createElement)(
1564
+ "div",
1565
+ { className: "mailharbor-kv" },
1566
+ (0, import_react7.createElement)("dt", null, "\u5F00\u59CB\u65F6\u95F4"),
1567
+ (0, import_react7.createElement)("dd", null, backup.startedAt ?? "\u2014"),
1568
+ (0, import_react7.createElement)("dt", null, "\u4E0A\u6B21\u6210\u529F"),
1569
+ (0, import_react7.createElement)("dd", null, backup.lastSuccessAt ?? "\u2014"),
1570
+ (0, import_react7.createElement)("dt", null, "\u65B0\u589E"),
1571
+ (0, import_react7.createElement)("dd", null, String(backup.added)),
1572
+ (0, import_react7.createElement)("dt", null, "\u603B\u6570"),
1573
+ (0, import_react7.createElement)("dd", null, String(backup.total))
1574
+ ),
1575
+ (0, import_react7.createElement)(
1576
+ "div",
1577
+ { className: "mailharbor-log" },
1578
+ backup.output.length === 0 ? "\uFF08\u7B49\u5F85\u8F93\u51FA\u2026\uFF09" : backup.output.join("\n")
1579
+ )
1580
+ ) : null,
1581
+ compose !== null ? (0, import_react7.createElement)(Composer, {
1582
+ account,
1583
+ initial: compose,
1584
+ onClose: () => setCompose(null),
1585
+ onSent: (message) => notice(message)
1586
+ }) : null,
1587
+ showAdd ? (0, import_react7.createElement)(AddAccountModal, {
1588
+ onClose: () => setShowAdd(false),
1589
+ onDone: (next) => {
1590
+ setState(next);
1591
+ refresh();
1592
+ }
1593
+ }) : null
1594
+ );
1595
+ }
1596
+
1597
+ // src/client/settings.tsx
1598
+ var import_react8 = require("react");
1599
+ function errorText4(error) {
1600
+ return error instanceof ApiError || error instanceof Error ? error.message : String(error);
1601
+ }
1602
+ function MailSettings() {
1603
+ const [state, setState] = (0, import_react8.useState)(null);
1604
+ const [settings, setSettings] = (0, import_react8.useState)(null);
1605
+ const [error, setError] = (0, import_react8.useState)("");
1606
+ const [toast, setToast] = (0, import_react8.useState)("");
1607
+ const [signature, setSignature] = (0, import_react8.useState)("");
1608
+ const [markOnOpen, setMarkOnOpen] = (0, import_react8.useState)(true);
1609
+ const [pollSeconds, setPollSeconds] = (0, import_react8.useState)(60);
1610
+ const [listLimit, setListLimit] = (0, import_react8.useState)(40);
1611
+ const [aiEnabled, setAiEnabled] = (0, import_react8.useState)(true);
1612
+ const [aiProvider, setAiProvider] = (0, import_react8.useState)("deepseek-account");
1613
+ const [aiModel, setAiModel] = (0, import_react8.useState)("deepseek-flash");
1614
+ const [editing, setEditing] = (0, import_react8.useState)(null);
1615
+ const [checkResult, setCheckResult] = (0, import_react8.useState)("");
1616
+ const [adding, setAdding] = (0, import_react8.useState)(false);
1617
+ const [tick, setTick] = (0, import_react8.useState)(0);
1618
+ (0, import_react8.useEffect)(() => {
1619
+ let cancelled = false;
1620
+ Promise.all([api.state(), api.settings()]).then(([appState, view]) => {
1621
+ if (cancelled) return;
1622
+ setState(appState);
1623
+ setSettings(view);
1624
+ setSignature(appState.settings.signature);
1625
+ setMarkOnOpen(view.read.markOnOpen);
1626
+ setPollSeconds(view.read.pollSeconds);
1627
+ setListLimit(view.render.listLimit);
1628
+ setAiEnabled(view.ai.enabled);
1629
+ setAiProvider(view.ai.provider);
1630
+ setAiModel(view.ai.model);
1631
+ }).catch((caught) => {
1632
+ if (!cancelled) setError(errorText4(caught));
1633
+ });
1634
+ return () => {
1635
+ cancelled = true;
1636
+ };
1637
+ }, [tick]);
1638
+ const reload = () => setTick((value) => value + 1);
1639
+ const notify = (message) => {
1640
+ setToast(message);
1641
+ window.setTimeout(() => {
1642
+ setToast("");
1643
+ }, 3e3);
1644
+ };
1645
+ const saveGeneral = async (patch) => {
1646
+ try {
1647
+ const result = await api.saveSettings(patch);
1648
+ setSettings(result.settings);
1649
+ notify("\u8BBE\u7F6E\u5DF2\u4FDD\u5B58");
1650
+ } catch (caught) {
1651
+ setError(errorText4(caught));
1652
+ }
1653
+ };
1654
+ const account = settings?.accounts.find((item) => item.id === settings.activeAccountId) ?? null;
1655
+ if (state === null || settings === null) {
1656
+ return (0, import_react8.createElement)("div", { className: "mailharbor mailharbor-settings" }, error === "" ? "\u6B63\u5728\u52A0\u8F7D\u8BBE\u7F6E\u2026" : error);
1657
+ }
1658
+ return (0, import_react8.createElement)(
1659
+ "div",
1660
+ { className: "mailharbor" },
1661
+ (0, import_react8.createElement)(
1662
+ "div",
1663
+ { className: "mailharbor-settings" },
1664
+ (0, import_react8.createElement)("h2", null, "MailHarbor"),
1665
+ (0, import_react8.createElement)("p", { className: "small muted" }, "\u591A\u8D26\u53F7 IMAP/SMTP \u90AE\u7BB1\u3001\u672C\u5730\u5907\u4EFD\u3001AI \u4E0E\u4EFB\u52A1\u3002\u8BBE\u7F6E\u4F1A\u7ACB\u5373\u751F\u6548\u3002"),
1666
+ toast === "" ? null : (0, import_react8.createElement)("div", { className: "small", style: { color: "var(--m-ok)" } }, toast),
1667
+ error === "" ? null : (0, import_react8.createElement)(ErrorBar, { message: error, onDismiss: () => setError("") }),
1668
+ (0, import_react8.createElement)(
1669
+ "div",
1670
+ { className: "mailharbor-card" },
1671
+ (0, import_react8.createElement)("h3", null, "\u8D26\u53F7"),
1672
+ settings.accounts.length === 0 ? (0, import_react8.createElement)(AddAccountForm, { onDone: () => reload() }) : settings.accounts.map(
1673
+ (item) => (0, import_react8.createElement)(AccountRow, {
1674
+ key: item.id,
1675
+ account: item,
1676
+ active: item.id === settings.activeAccountId,
1677
+ loggedIn: item.id === settings.activeAccountId ? state.loggedIn : void 0,
1678
+ editing: editing === item.id,
1679
+ checkResult: editing === item.id ? checkResult : "",
1680
+ onToggleEdit: () => {
1681
+ setEditing(editing === item.id ? null : item.id);
1682
+ setCheckResult("");
1683
+ },
1684
+ onActivate: async () => {
1685
+ await api.activateAccount(item.id);
1686
+ reload();
1687
+ },
1688
+ onLogin: async (password, remember) => {
1689
+ await api.login(item.id, password, remember);
1690
+ reload();
1691
+ },
1692
+ onLogout: async () => {
1693
+ await api.logout(item.id);
1694
+ reload();
1695
+ },
1696
+ onCheck: async () => {
1697
+ try {
1698
+ const result = await api.check(item.id);
1699
+ setCheckResult(
1700
+ `IMAP \u6B63\u5E38\uFF08${String(result.imap.mailboxes.length)} \u4E2A\u6587\u4EF6\u5939\uFF09\xB7 SMTP ${result.smtp.ok ? "\u6B63\u5E38" : `\u5931\u8D25: ${result.smtp.error ?? ""}`}`
1701
+ );
1702
+ } catch (caught) {
1703
+ setCheckResult(`\u5931\u8D25: ${errorText4(caught)}`);
1704
+ }
1705
+ },
1706
+ onSave: async (patch) => {
1707
+ await api.updateAccount(item.id, patch);
1708
+ reload();
1709
+ },
1710
+ onRemove: async () => {
1711
+ if (!window.confirm(`\u79FB\u9664\u8D26\u53F7 ${item.email}\uFF1F\u672C\u5730\u6570\u636E\u76EE\u5F55\u4F1A\u4FDD\u7559\u3002`)) return;
1712
+ await api.deleteAccount(item.id);
1713
+ reload();
1714
+ }
1715
+ })
1716
+ ),
1717
+ adding ? (0, import_react8.createElement)(AddAccountForm, {
1718
+ onDone: () => {
1719
+ setAdding(false);
1720
+ reload();
1721
+ },
1722
+ onCancel: () => setAdding(false)
1723
+ }) : (0, import_react8.createElement)("button", { className: "ghost", onClick: () => setAdding(true) }, "+ \u6DFB\u52A0\u8D26\u53F7")
1724
+ ),
1725
+ (0, import_react8.createElement)(
1726
+ "div",
1727
+ { className: "mailharbor-card" },
1728
+ (0, import_react8.createElement)("h3", null, "\u5199\u4FE1\u4E0E\u9605\u8BFB"),
1729
+ (0, import_react8.createElement)(
1730
+ Field,
1731
+ { label: "\u7B7E\u540D\uFF08\u7EAF\u6587\u672C\uFF0C\u53D1\u9001\u65F6\u8FFD\u52A0\u5230\u6B63\u6587\u672B\u5C3E\uFF09" },
1732
+ (0, import_react8.createElement)("textarea", {
1733
+ rows: 4,
1734
+ value: signature,
1735
+ onChange: (event) => setSignature(event.target.value)
1736
+ })
1737
+ ),
1738
+ (0, import_react8.createElement)(
1739
+ "div",
1740
+ { style: { display: "flex", gap: "10px", alignItems: "center", flexWrap: "wrap" } },
1741
+ (0, import_react8.createElement)("button", { className: "ghost", onClick: () => void saveGeneral({ signature }) }, "\u4FDD\u5B58\u7B7E\u540D"),
1742
+ (0, import_react8.createElement)(Toggle, {
1743
+ checked: markOnOpen,
1744
+ onChange: (value) => {
1745
+ setMarkOnOpen(value);
1746
+ void saveGeneral({ read: { markOnOpen: value } });
1747
+ },
1748
+ label: "\u6253\u5F00\u90AE\u4EF6\u65F6\u6807\u4E3A\u5DF2\u8BFB"
1749
+ })
1750
+ ),
1751
+ (0, import_react8.createElement)(
1752
+ "div",
1753
+ { className: "mailharbor-grid" },
1754
+ (0, import_react8.createElement)(
1755
+ Field,
1756
+ { label: "\u5217\u8868\u6BCF\u9875\u6570\u91CF" },
1757
+ (0, import_react8.createElement)("input", {
1758
+ type: "number",
1759
+ min: 10,
1760
+ max: 200,
1761
+ value: listLimit,
1762
+ onChange: (event) => setListLimit(Number(event.target.value)),
1763
+ onBlur: () => void saveGeneral({ render: { listLimit } })
1764
+ })
1765
+ ),
1766
+ (0, import_react8.createElement)(
1767
+ Field,
1768
+ { label: "\u672A\u8BFB\u89D2\u6807\u8F6E\u8BE2\uFF08\u79D2\uFF09" },
1769
+ (0, import_react8.createElement)("input", {
1770
+ type: "number",
1771
+ min: 15,
1772
+ max: 3600,
1773
+ value: pollSeconds,
1774
+ onChange: (event) => setPollSeconds(Number(event.target.value)),
1775
+ onBlur: () => void saveGeneral({ read: { pollSeconds } })
1776
+ })
1777
+ )
1778
+ )
1779
+ ),
1780
+ (0, import_react8.createElement)(
1781
+ "div",
1782
+ { className: "mailharbor-card" },
1783
+ (0, import_react8.createElement)("h3", null, "AI"),
1784
+ (0, import_react8.createElement)(Toggle, {
1785
+ checked: aiEnabled,
1786
+ onChange: (value) => {
1787
+ setAiEnabled(value);
1788
+ void saveGeneral({ ai: { enabled: value } });
1789
+ },
1790
+ label: "\u542F\u7528 AI\uFF08\u6458\u8981\u3001\u8D77\u8349\u3001\u6DA6\u8272\u3001\u81EA\u7136\u8BED\u8A00\u641C\u7D22\uFF09"
1791
+ }),
1792
+ (0, import_react8.createElement)(
1793
+ "div",
1794
+ { className: "mailharbor-grid" },
1795
+ (0, import_react8.createElement)(
1796
+ Field,
1797
+ { label: "Provider" },
1798
+ (0, import_react8.createElement)("input", {
1799
+ value: aiProvider,
1800
+ onChange: (event) => setAiProvider(event.target.value),
1801
+ onBlur: () => void saveGeneral({ ai: { provider: aiProvider } })
1802
+ })
1803
+ ),
1804
+ (0, import_react8.createElement)(
1805
+ Field,
1806
+ { label: "\u6A21\u578B" },
1807
+ (0, import_react8.createElement)("input", {
1808
+ value: aiModel,
1809
+ onChange: (event) => setAiModel(event.target.value),
1810
+ onBlur: () => void saveGeneral({ ai: { model: aiModel } })
1811
+ })
1812
+ )
1813
+ ),
1814
+ (0, import_react8.createElement)(
1815
+ "p",
1816
+ { className: "small muted" },
1817
+ "AI \u8C03\u7528\u8D70 DSH \u81EA\u5DF1\u7684\u6A21\u578B\u670D\u52A1\uFF0C\u4E0D\u989D\u5916\u4FDD\u5B58\u5BC6\u94A5\u3002AI \u9762\u677F\u529F\u80FD\u5C06\u5728 v0.2 \u63D0\u4F9B\u3002"
1818
+ )
1819
+ ),
1820
+ (0, import_react8.createElement)(
1821
+ "div",
1822
+ { className: "mailharbor-card" },
1823
+ (0, import_react8.createElement)("h3", null, "\u6570\u636E"),
1824
+ (0, import_react8.createElement)(
1825
+ "dl",
1826
+ { className: "mailharbor-kv" },
1827
+ (0, import_react8.createElement)("dt", null, "\u6570\u636E\u76EE\u5F55"),
1828
+ (0, import_react8.createElement)("dd", null, settings.dataDir),
1829
+ (0, import_react8.createElement)("dt", null, "\u914D\u7F6E\u6587\u4EF6"),
1830
+ (0, import_react8.createElement)("dd", null, settings.configFile),
1831
+ (0, import_react8.createElement)("dt", null, "\u5F53\u524D\u8D26\u53F7\u6570\u636E\u5E93"),
1832
+ (0, import_react8.createElement)("dd", null, account === null ? "\u2014" : `${settings.dataDir}/accounts/${account.id}/mail.db`),
1833
+ (0, import_react8.createElement)("dt", null, "\u63D2\u4EF6\u7248\u672C"),
1834
+ (0, import_react8.createElement)("dd", null, state.version)
1835
+ ),
1836
+ (0, import_react8.createElement)(
1837
+ "div",
1838
+ { style: { display: "flex", gap: "8px", flexWrap: "wrap" } },
1839
+ account === null ? null : (0, import_react8.createElement)(
1840
+ "button",
1841
+ {
1842
+ className: "ghost",
1843
+ onClick: async () => {
1844
+ try {
1845
+ const status = await api.startBackup(account.id);
1846
+ notify(status.status.message === "" ? "\u5907\u4EFD\u5DF2\u5F00\u59CB" : status.status.message);
1847
+ reload();
1848
+ } catch (caught) {
1849
+ setError(errorText4(caught));
1850
+ }
1851
+ }
1852
+ },
1853
+ "\u7ACB\u5373\u5907\u4EFD\u5F53\u524D\u8D26\u53F7"
1854
+ )
1855
+ )
1856
+ )
1857
+ )
1858
+ );
1859
+ }
1860
+ function AccountRow({
1861
+ account,
1862
+ active,
1863
+ loggedIn,
1864
+ editing,
1865
+ checkResult,
1866
+ onToggleEdit,
1867
+ onActivate,
1868
+ onLogin,
1869
+ onLogout,
1870
+ onCheck,
1871
+ onSave,
1872
+ onRemove
1873
+ }) {
1874
+ const [password, setPassword] = (0, import_react8.useState)("");
1875
+ const [remember, setRemember] = (0, import_react8.useState)(true);
1876
+ const [label, setLabel] = (0, import_react8.useState)(account.label);
1877
+ const [username, setUsername] = (0, import_react8.useState)(account.username);
1878
+ const [imapHost, setImapHost] = (0, import_react8.useState)(account.imap.host);
1879
+ const [imapPort, setImapPort] = (0, import_react8.useState)(String(account.imap.port));
1880
+ const [imapSecure, setImapSecure] = (0, import_react8.useState)(account.imap.secure);
1881
+ const [smtpHost, setSmtpHost] = (0, import_react8.useState)(account.smtp.host);
1882
+ const [smtpPort, setSmtpPort] = (0, import_react8.useState)(String(account.smtp.port));
1883
+ const [smtpSecure, setSmtpSecure] = (0, import_react8.useState)(account.smtp.secure);
1884
+ const [fromName, setFromName] = (0, import_react8.useState)(account.smtp.fromName);
1885
+ const [busy, setBusy] = (0, import_react8.useState)(false);
1886
+ (0, import_react8.useEffect)(() => {
1887
+ setLabel(account.label);
1888
+ setUsername(account.username);
1889
+ setImapHost(account.imap.host);
1890
+ setImapPort(String(account.imap.port));
1891
+ setImapSecure(account.imap.secure);
1892
+ setSmtpHost(account.smtp.host);
1893
+ setSmtpPort(String(account.smtp.port));
1894
+ setSmtpSecure(account.smtp.secure);
1895
+ setFromName(account.smtp.fromName);
1896
+ }, [account]);
1897
+ const wrap = async (operation) => {
1898
+ setBusy(true);
1899
+ try {
1900
+ await operation();
1901
+ } finally {
1902
+ setBusy(false);
1903
+ }
1904
+ };
1905
+ return (0, import_react8.createElement)(
1906
+ "div",
1907
+ { className: "mailharbor-card", style: { background: "var(--m-bg-1)" } },
1908
+ (0, import_react8.createElement)(
1909
+ "div",
1910
+ { className: "mailharbor-account-row" },
1911
+ (0, import_react8.createElement)(Avatar, { email: account.email, size: 30 }),
1912
+ (0, import_react8.createElement)(
1913
+ "div",
1914
+ { style: { display: "flex", flexDirection: "column", minWidth: 0 } },
1915
+ (0, import_react8.createElement)("strong", null, account.label),
1916
+ (0, import_react8.createElement)("span", { className: "small muted" }, account.email)
1917
+ ),
1918
+ active ? (0, import_react8.createElement)("span", { className: "badge ok" }, "\u5F53\u524D\u8D26\u53F7") : null,
1919
+ loggedIn === false ? (0, import_react8.createElement)("span", { className: "badge warn" }, "\u672A\u767B\u5F55") : null,
1920
+ (0, import_react8.createElement)("span", { className: "spacer" }),
1921
+ active ? null : (0, import_react8.createElement)("button", { className: "ghost small", onClick: () => void wrap(onActivate) }, "\u8BBE\u4E3A\u5F53\u524D"),
1922
+ (0, import_react8.createElement)("button", { className: "ghost small", onClick: () => void wrap(onCheck) }, "\u68C0\u67E5\u8FDE\u63A5"),
1923
+ (0, import_react8.createElement)("button", { className: "ghost small", onClick: onToggleEdit }, editing ? "\u6536\u8D77" : "\u7F16\u8F91")
1924
+ ),
1925
+ editing ? (0, import_react8.createElement)(
1926
+ "div",
1927
+ { style: { display: "flex", flexDirection: "column", gap: "10px" } },
1928
+ (0, import_react8.createElement)(
1929
+ "div",
1930
+ { className: "mailharbor-grid" },
1931
+ (0, import_react8.createElement)(
1932
+ Field,
1933
+ { label: "\u663E\u793A\u540D\u79F0" },
1934
+ (0, import_react8.createElement)("input", {
1935
+ value: label,
1936
+ onChange: (event) => setLabel(event.target.value)
1937
+ })
1938
+ ),
1939
+ (0, import_react8.createElement)(
1940
+ Field,
1941
+ { label: "\u767B\u5F55\u7528\u6237\u540D" },
1942
+ (0, import_react8.createElement)("input", {
1943
+ value: username,
1944
+ onChange: (event) => setUsername(event.target.value)
1945
+ })
1946
+ )
1947
+ ),
1948
+ (0, import_react8.createElement)(
1949
+ "div",
1950
+ { className: "mailharbor-grid three" },
1951
+ (0, import_react8.createElement)(
1952
+ Field,
1953
+ { label: "IMAP \u4E3B\u673A" },
1954
+ (0, import_react8.createElement)("input", {
1955
+ value: imapHost,
1956
+ onChange: (event) => setImapHost(event.target.value)
1957
+ })
1958
+ ),
1959
+ (0, import_react8.createElement)(
1960
+ Field,
1961
+ { label: "\u7AEF\u53E3" },
1962
+ (0, import_react8.createElement)("input", {
1963
+ value: imapPort,
1964
+ onChange: (event) => setImapPort(event.target.value)
1965
+ })
1966
+ ),
1967
+ (0, import_react8.createElement)(Toggle, { checked: imapSecure, onChange: setImapSecure, label: "TLS" })
1968
+ ),
1969
+ (0, import_react8.createElement)(
1970
+ "div",
1971
+ { className: "mailharbor-grid three" },
1972
+ (0, import_react8.createElement)(
1973
+ Field,
1974
+ { label: "SMTP \u4E3B\u673A" },
1975
+ (0, import_react8.createElement)("input", {
1976
+ value: smtpHost,
1977
+ onChange: (event) => setSmtpHost(event.target.value)
1978
+ })
1979
+ ),
1980
+ (0, import_react8.createElement)(
1981
+ Field,
1982
+ { label: "\u7AEF\u53E3" },
1983
+ (0, import_react8.createElement)("input", {
1984
+ value: smtpPort,
1985
+ onChange: (event) => setSmtpPort(event.target.value)
1986
+ })
1987
+ ),
1988
+ (0, import_react8.createElement)(Toggle, { checked: smtpSecure, onChange: setSmtpSecure, label: "TLS" })
1989
+ ),
1990
+ (0, import_react8.createElement)(
1991
+ Field,
1992
+ { label: "\u53D1\u4EF6\u4EBA\u540D\u79F0" },
1993
+ (0, import_react8.createElement)("input", {
1994
+ value: fromName,
1995
+ onChange: (event) => setFromName(event.target.value)
1996
+ })
1997
+ ),
1998
+ (0, import_react8.createElement)(
1999
+ "div",
2000
+ { style: { display: "flex", gap: "8px", alignItems: "center", flexWrap: "wrap" } },
2001
+ (0, import_react8.createElement)(
2002
+ "button",
2003
+ {
2004
+ className: "primary small",
2005
+ disabled: busy,
2006
+ onClick: () => void wrap(async () => {
2007
+ await onSave({
2008
+ label,
2009
+ username,
2010
+ imap: { host: imapHost, port: Number(imapPort) || 993, secure: imapSecure },
2011
+ smtp: { host: smtpHost, port: Number(smtpPort) || 465, secure: smtpSecure, fromName }
2012
+ });
2013
+ })
2014
+ },
2015
+ "\u4FDD\u5B58\u8D26\u53F7"
2016
+ ),
2017
+ (0, import_react8.createElement)(
2018
+ "button",
2019
+ {
2020
+ className: "ghost small danger",
2021
+ onClick: () => void wrap(onRemove)
2022
+ },
2023
+ "\u79FB\u9664\u8D26\u53F7"
2024
+ )
2025
+ ),
2026
+ (0, import_react8.createElement)("hr", { style: { border: 0, borderTop: "1px solid var(--m-line)", width: "100%" } }),
2027
+ (0, import_react8.createElement)(
2028
+ Field,
2029
+ { label: "\u5BC6\u7801 / \u5E94\u7528\u4E13\u7528\u5BC6\u7801\uFF08\u4E0D\u5199\u5165\u914D\u7F6E\u6587\u4EF6\uFF09" },
2030
+ (0, import_react8.createElement)("input", {
2031
+ type: "password",
2032
+ value: password,
2033
+ onChange: (event) => setPassword(event.target.value)
2034
+ })
2035
+ ),
2036
+ (0, import_react8.createElement)(
2037
+ "div",
2038
+ { style: { display: "flex", gap: "8px", alignItems: "center" } },
2039
+ (0, import_react8.createElement)(Toggle, { checked: remember, onChange: setRemember, label: "\u8BB0\u4F4F\u5BC6\u7801" }),
2040
+ (0, import_react8.createElement)(
2041
+ "button",
2042
+ {
2043
+ className: "primary small",
2044
+ disabled: busy || password === "",
2045
+ onClick: () => void wrap(async () => {
2046
+ await onLogin(password, remember);
2047
+ setPassword("");
2048
+ })
2049
+ },
2050
+ "\u767B\u5F55"
2051
+ ),
2052
+ (0, import_react8.createElement)("button", { className: "ghost small", onClick: () => void wrap(onLogout) }, "\u9000\u51FA\u767B\u5F55")
2053
+ ),
2054
+ checkResult === "" ? null : (0, import_react8.createElement)("div", { className: "small muted" }, checkResult)
2055
+ ) : null
2056
+ );
2057
+ }
2058
+
2059
+ // src/client/index.tsx
2060
+ var VERSION = "0.2.0";
2061
+ var inject = ["slots"];
2062
+ var PANEL_ID = "mailharbor";
2063
+ var STYLE_ID = "mailharbor-style";
2064
+ function ensureStyles() {
2065
+ if (document.getElementById(STYLE_ID) !== null) return;
2066
+ const element = document.createElement("style");
2067
+ element.id = STYLE_ID;
2068
+ element.textContent = styles_default;
2069
+ document.head.append(element);
2070
+ }
2071
+ function apply(ctx) {
2072
+ ensureStyles();
2073
+ ctx.slots.inject("main", () => ctx.slots.register({ name: "main", key: PANEL_ID }, MailPanel));
2074
+ ctx.slots.inject(
2075
+ "settings.section",
2076
+ () => ctx.slots.register(
2077
+ {
2078
+ name: "settings.section",
2079
+ id: PANEL_ID,
2080
+ order: 35,
2081
+ label: () => "MailHarbor"
2082
+ },
2083
+ MailSettings
2084
+ )
2085
+ );
2086
+ ctx.slots.inject(
2087
+ "sidebar.panellist",
2088
+ () => ctx.slots.register(
2089
+ {
2090
+ name: "sidebar.panellist",
2091
+ id: PANEL_ID,
2092
+ order: 20,
2093
+ label: "MailHarbor"
2094
+ },
2095
+ MailIcon
2096
+ )
2097
+ );
2098
+ }
2099
+
2100
+ return module.exports;
2101
+ },
2102
+ });
2103
+
2104
+ //# sourceMappingURL=client.js.map