dsh-email 0.17.2 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.en.md +7 -4
- package/README.md +7 -4
- package/docs/AGENTMAIL.en.md +37 -0
- package/docs/AGENTMAIL.md +41 -0
- package/docs/USAGE.en.md +13 -1
- package/docs/USAGE.md +11 -1
- package/docs/VALIDATION.md +2 -0
- package/docs/validation/0.18.0.md +23 -0
- package/lib/agentmail.d.ts +33 -0
- package/lib/agentmail.js +407 -0
- package/lib/client.js +129 -31
- package/lib/config.d.ts +4 -0
- package/lib/config.js +12 -2
- package/lib/draft-store.d.ts +5 -0
- package/lib/draft-store.js +4 -0
- package/lib/draft-web.js +15 -6
- package/lib/host-config.js +1 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +12 -2
- package/lib/mail-client.d.ts +8 -3
- package/lib/mail-client.js +83 -20
- package/lib/mail-service.d.ts +50 -0
- package/lib/mail-service.js +128 -0
- package/lib/mailbox-web.d.ts +11 -0
- package/lib/mailbox-web.js +219 -0
- package/lib/public-download.d.ts +3 -0
- package/lib/public-download.js +77 -0
- package/lib/runtime.d.ts +2 -2
- package/lib/runtime.js +7 -3
- package/lib/settings.js +1 -1
- package/lib/tool-contract.d.ts +37 -8
- package/lib/tool-contract.js +48 -18
- package/lib/tools.js +14 -10
- package/lib/types.d.ts +44 -12
- package/lib/web.d.ts +9 -1
- package/lib/web.js +20 -2
- package/package.json +4 -3
package/lib/client.js
CHANGED
|
@@ -12,10 +12,10 @@ const h = React.createElement;
|
|
|
12
12
|
const __FR_KEY = 'dsh-first-run:dsh-email:v1';
|
|
13
13
|
const __FR_TITLE = '邮件 · 首次使用(4 步)';
|
|
14
14
|
const __FR_STEPS = [
|
|
15
|
-
{ title: '先把账号填上', body: '
|
|
16
|
-
{ title: '看一眼收件箱', body: '
|
|
15
|
+
{ title: '先把账号填上', body: '选择邮箱服务商或 AgentMail,填写对应授权码、登录信息或 API Key,再测试连接。' },
|
|
16
|
+
{ title: '看一眼收件箱', body: '连接成功后切到「收发邮件」,选择账号与文件夹;点开一封邮件即可阅读正文。' },
|
|
17
17
|
{ title: '让它替你整理', body: '在对话里直接说你要做的事,比在页面里逐个点更快。', tryIt: '把今天的未读邮件按主题归纳一下' },
|
|
18
|
-
{ title: '
|
|
18
|
+
{ title: '发信前先看草稿', body: '用「写邮件」或「回复到草稿」准备内容,核对后再确认发送;模型发送工具仍遵守审批设置,收件人规则在对应页配置。' },
|
|
19
19
|
];
|
|
20
20
|
|
|
21
21
|
// ---- first-run guide (shared snippet, keep in sync across plugins) ----
|
|
@@ -53,19 +53,20 @@ function __FirstRunCard() {
|
|
|
53
53
|
const [index, setIndex] = React.useState(0);
|
|
54
54
|
React.useEffect(() => { __frEnsureCss(); }, []);
|
|
55
55
|
if (done) return null;
|
|
56
|
-
const
|
|
57
|
-
const
|
|
56
|
+
const steps = UI_LANG === "en" ? [{title:"Add an account",body:"Choose your mail provider or AgentMail and enter its credentials. Test the connection before browsing."},{title:"Open your mailbox",body:"Use Mailbox to browse Inbox and Sent, search, and read a message."},{title:"Let DSH organize mail",body:"Ask DSH to summarize unread mail or find a message."},{title:"Review before sending",body:"Compose or reply in Drafts. Review recipients, content and attachments, then confirm sending."}] : __FR_STEPS;
|
|
57
|
+
const step = steps[index];
|
|
58
|
+
const last = index >= steps.length - 1;
|
|
58
59
|
const close = () => { __frWrite(); setDone(true); };
|
|
59
60
|
return h('div', { className: 'fr-card' },
|
|
60
|
-
h('div', { className: 'fr-head' }, h('span', { className: 'fr-dot' }), h('span', { className: 'fr-title' }, __FR_TITLE), h('span', { className: 'fr-count' }, (index + 1) + ' / ' +
|
|
61
|
+
h('div', { className: 'fr-head' }, h('span', { className: 'fr-dot' }), h('span', { className: 'fr-title' }, UI_LANG === 'en' ? 'Email · Getting started (4 steps)' : __FR_TITLE), h('span', { className: 'fr-count' }, (index + 1) + ' / ' + steps.length)),
|
|
61
62
|
h('div', { className: 'fr-step' }, step.title),
|
|
62
63
|
h('div', { className: 'fr-body' }, step.body),
|
|
63
64
|
step.tryIt ? h('div', { className: 'fr-try' }, h('span', null, '可以直接说'), h('code', null, '「' + step.tryIt + '」')) : null,
|
|
64
65
|
h('div', { className: 'fr-bar' },
|
|
65
|
-
h('div', { className: 'fr-progress' },
|
|
66
|
-
h('button', { className: 'fr-btn ghost', type: 'button', onClick: close }, '跳过'),
|
|
67
|
-
index > 0 ? h('button', { className: 'fr-btn', type: 'button', onClick: () => setIndex(index - 1) }, '上一步') : null,
|
|
68
|
-
h('button', { className: 'fr-btn primary', type: 'button', onClick: () => { if (last) close(); else setIndex(index + 1); } }, last ? '开始使用' : '下一步')));
|
|
66
|
+
h('div', { className: 'fr-progress' }, steps.map((_, i) => h('span', { key: i, className: 'fr-seg' + (i <= index ? ' on' : '') }))),
|
|
67
|
+
h('button', { className: 'fr-btn ghost', type: 'button', onClick: close }, UI_LANG === 'en' ? 'Skip' : '跳过'),
|
|
68
|
+
index > 0 ? h('button', { className: 'fr-btn', type: 'button', onClick: () => setIndex(index - 1) }, UI_LANG === 'en' ? 'Previous' : '上一步') : null,
|
|
69
|
+
h('button', { className: 'fr-btn primary', type: 'button', onClick: () => { if (last) close(); else setIndex(index + 1); } }, last ? (UI_LANG === 'en' ? 'Start' : '开始使用') : (UI_LANG === 'en' ? 'Next' : '下一步'))));
|
|
69
70
|
}
|
|
70
71
|
function __withFirstRun(Component) {
|
|
71
72
|
return function FirstRunWrapped(props) {
|
|
@@ -92,7 +93,8 @@ function emailError(error) {
|
|
|
92
93
|
if (/预览已过期|draft-preview-expired/.test(message + ' ' + (error?.code || ''))) return 'The send review expired or was already used. Review the current draft again.';
|
|
93
94
|
if (/改变|已更新|冲突|重新加载/.test(message)) return 'The draft, account or rules changed. Reload the latest version and review your input before retrying.';
|
|
94
95
|
if (/请先填写并保存邮件主题/.test(message)) return 'Fill in and save the email subject first.';
|
|
95
|
-
if (/附件/.test(message)) return 'Check the attachments
|
|
96
|
+
if (/附件/.test(message)) return 'Check the attachments and the selected account limit (up to 4 MiB for AgentMail, 20 MiB for IMAP). Save first, use at most 10 files, and remove unreadable or oversized files.';
|
|
97
|
+
if (/AgentMail/.test(message)) { const status = message.match(/HTTP \d+/)?.[0]; return 'AgentMail request failed' + (status ? ' (' + status + ')' : '') + '. Check the API region, key, inbox and service status. For sending, check Sent before trying again.'; }
|
|
96
98
|
if (/账号|配置不完整|尚未登录|OAuth|设备码/.test(message)) return 'Check the selected email account. Complete its credentials or OAuth sign-in in Account settings, then test the connection.';
|
|
97
99
|
if (/收件人|邮箱地址/.test(message)) return 'Check the recipient addresses and trusted-recipient rules before reviewing the draft again.';
|
|
98
100
|
return 'The email operation failed. Check the account settings and DSH logs, then retry. For a send operation, check the mailbox before retrying.';
|
|
@@ -900,6 +902,7 @@ function draftsFromCards(list) {
|
|
|
900
902
|
*/
|
|
901
903
|
function providerOptions(presets) {
|
|
902
904
|
const out = PROVIDERS.map(([value, labelKey]) => ({ value, label: t(labelKey) }));
|
|
905
|
+
out.push({value:"agentmail",label:"AgentMail (US API)"},{value:"agentmail-eu",label:"AgentMail (EU API)"});
|
|
903
906
|
const custom = (presets && presets.custom) || {};
|
|
904
907
|
for (const name of Object.keys(custom)) {
|
|
905
908
|
if (name === "") continue;
|
|
@@ -1017,6 +1020,7 @@ function renameDrafts(drafts, oldName, nextName) {
|
|
|
1017
1020
|
* presets.custom(值还是 undefined,端点要等下一次快照),它已经是合法预设了。
|
|
1018
1021
|
*/
|
|
1019
1022
|
function accountSummary(draft, presets) {
|
|
1023
|
+
if(draft.provider==="agentmail"||draft.provider==="agentmail-eu")return {label:draft.provider==="agentmail-eu"?"AgentMail EU":"AgentMail US",isCustom:false};
|
|
1020
1024
|
const provider = draft.provider || "";
|
|
1021
1025
|
const entry = PROVIDERS.filter(([value]) => value === provider)[0];
|
|
1022
1026
|
if (entry) return { label: t(entry[1]), isCustom: false };
|
|
@@ -1039,6 +1043,7 @@ function presetEndpointText(endpoint) {
|
|
|
1039
1043
|
* 保存后的新快照优先,避免自定义预设更新后继续显示旧地址。
|
|
1040
1044
|
*/
|
|
1041
1045
|
function providerMetaText(draft, presets, cards) {
|
|
1046
|
+
if(draft.provider==="agentmail"||draft.provider==="agentmail-eu")return draft.provider==="agentmail-eu"?"AgentMail · api.agentmail.eu":"AgentMail · api.agentmail.to";
|
|
1042
1047
|
const provider = String(draft.provider || "").trim();
|
|
1043
1048
|
const saved = (cards || []).find(card => card.name === draft.name)
|
|
1044
1049
|
|| (cards || []).find(card => card.name === draft.originalName);
|
|
@@ -1722,7 +1727,7 @@ function AccountCardsEditor(props) {
|
|
|
1722
1727
|
setRowStatus((cur) => Object.assign({}, cur, {
|
|
1723
1728
|
[draftName]: {
|
|
1724
1729
|
kind: "success",
|
|
1725
|
-
text: t("card.testOk", { ms: result.ms, host: result.imapHost, port: result.imapPort }),
|
|
1730
|
+
text: result.apiHost ? dt("API 连接成功 · ","API connected · ")+result.apiHost : t("card.testOk", { ms: result.ms, host: result.imapHost, port: result.imapPort }),
|
|
1726
1731
|
},
|
|
1727
1732
|
}));
|
|
1728
1733
|
} catch (e) {
|
|
@@ -1866,7 +1871,7 @@ function AccountCardsEditor(props) {
|
|
|
1866
1871
|
h("label", null, t("card.addressLabel")),
|
|
1867
1872
|
fieldInput("text", draft.user, (v) => patchDraft(name, { user: v }), "you@example.com"),
|
|
1868
1873
|
]),
|
|
1869
|
-
h("div", { className: "dshe-field" }, [
|
|
1874
|
+
!draft.provider?.startsWith("agentmail") && h("div", { className: "dshe-field" }, [
|
|
1870
1875
|
h("label", null, t("card.senderNameLabel")),
|
|
1871
1876
|
fieldInput("text", draft.senderName, (v) => patchDraft(name, { senderName: v }), t("card.senderNamePlaceholder")),
|
|
1872
1877
|
h("div", { className: "dshe-hint" }, t("card.senderNameHint")),
|
|
@@ -1912,32 +1917,32 @@ function AccountCardsEditor(props) {
|
|
|
1912
1917
|
})
|
|
1913
1918
|
: h(React.Fragment, null, [
|
|
1914
1919
|
h("div", { className: "dshe-field" }, [
|
|
1915
|
-
h("label", null, t("card.passwordLabel")),
|
|
1920
|
+
h("label", null, draft.provider?.startsWith("agentmail") ? "AgentMail API Key" : t("card.passwordLabel")),
|
|
1916
1921
|
h("input", {
|
|
1917
1922
|
type: "password",
|
|
1918
1923
|
value: draft.password,
|
|
1919
1924
|
disabled: busy !== "",
|
|
1920
|
-
placeholder: t("card.passwordPlaceholder"),
|
|
1925
|
+
placeholder: draft.provider?.startsWith("agentmail") ? dt("输入 API Key;留空保留已存密钥", "API key; leave blank to retain saved key") : t("card.passwordPlaceholder"),
|
|
1921
1926
|
onChange: (e) => patchDraft(name, { password: e.target.value }),
|
|
1922
1927
|
}),
|
|
1923
1928
|
h("div", { className: "dshe-hint" },
|
|
1924
1929
|
view.hasPassword
|
|
1925
1930
|
? t("card.passwordSaved")
|
|
1926
|
-
: t("card.passwordHint")),
|
|
1931
|
+
: draft.provider?.startsWith("agentmail") ? dt("从 AgentMail 控制台取得 API Key。API Key 保存在本机账号配置中,由 DSH 后端调用所选区域的 API。", "Use an API key from the AgentMail console. The key is stored in the local account configuration; the DSH backend calls the selected region.") : t("card.passwordHint")),
|
|
1927
1932
|
]),
|
|
1928
1933
|
// 别名/中继:登录名与登录密码都只在密码类账号上有意义(OAuth2 用 token 登录)。
|
|
1929
|
-
h("div", { className: "dshe-field" }, [
|
|
1934
|
+
!draft.provider?.startsWith("agentmail") && h("div", { className: "dshe-field" }, [
|
|
1930
1935
|
h("label", null, t("card.authUserLabel")),
|
|
1931
1936
|
fieldInput("text", draft.authUser, (v) => patchDraft(name, { authUser: v }), t("card.authUserPlaceholder")),
|
|
1932
1937
|
h("div", { className: "dshe-hint" }, t("card.authUserHint")),
|
|
1933
1938
|
]),
|
|
1934
|
-
h("div", { className: "dshe-field" }, [
|
|
1939
|
+
!draft.provider?.startsWith("agentmail") && h("div", { className: "dshe-field" }, [
|
|
1935
1940
|
h("label", null, t("card.authPasswordLabel")),
|
|
1936
1941
|
h("input", {
|
|
1937
1942
|
type: "password",
|
|
1938
1943
|
value: draft.authPassword,
|
|
1939
1944
|
disabled: busy !== "",
|
|
1940
|
-
placeholder: t("card.passwordPlaceholder"),
|
|
1945
|
+
placeholder: draft.provider?.startsWith("agentmail") ? dt("输入 API Key;留空保留已存密钥", "API key; leave blank to retain saved key") : t("card.passwordPlaceholder"),
|
|
1941
1946
|
onChange: (e) => patchDraft(name, { authPassword: e.target.value }),
|
|
1942
1947
|
}),
|
|
1943
1948
|
h("div", { className: "dshe-hint" },
|
|
@@ -1946,7 +1951,7 @@ function AccountCardsEditor(props) {
|
|
|
1946
1951
|
: t("card.authPasswordHint")),
|
|
1947
1952
|
]),
|
|
1948
1953
|
]),
|
|
1949
|
-
h("div", { className: "dshe-field" }, [
|
|
1954
|
+
!draft.provider?.startsWith("agentmail") && h("div", { className: "dshe-field" }, [
|
|
1950
1955
|
h("label", null, t("card.inboxLabel")),
|
|
1951
1956
|
fieldInput("text", draft.inboxFolder, (v) => patchDraft(name, { inboxFolder: v }), "INBOX"),
|
|
1952
1957
|
]),
|
|
@@ -2228,6 +2233,93 @@ function ServerPresetsEditor(props) {
|
|
|
2228
2233
|
]);
|
|
2229
2234
|
}
|
|
2230
2235
|
|
|
2236
|
+
const MAILBOX_ROUTE = "api/dsh-email/mailbox";
|
|
2237
|
+
async function mailboxApi(action, payload = {}, signal) {
|
|
2238
|
+
const response = await fetch(MAILBOX_ROUTE, { credentials: "same-origin", method: "POST", headers: { "Content-Type": "application/json", "x-dsh-email-mailbox": "1" }, body: JSON.stringify({ ...payload, action }), signal });
|
|
2239
|
+
if (action === "download" && response.ok) return response.blob();
|
|
2240
|
+
const result = await response.json();
|
|
2241
|
+
if (!response.ok || !result.ok) throw new Error(result.error?.message || dt("邮箱操作失败", "Mailbox action failed"));
|
|
2242
|
+
return result.value;
|
|
2243
|
+
}
|
|
2244
|
+
function MailboxWorkbench({ active, onConfigure, onDraft }) {
|
|
2245
|
+
const [accounts, setAccounts] = useState([]), [account, setAccount] = useState(""), [folders, setFolders] = useState([]), [folder, setFolder] = useState("");
|
|
2246
|
+
const [input, setInput] = useState(""), [query, setQuery] = useState(""), [offset, setOffset] = useState(0), [scanOffset, setScanOffset] = useState(0), [unread, setUnread] = useState(false), [refresh, setRefresh] = useState(0);
|
|
2247
|
+
const [result, setResult] = useState(null), [selected, setSelected] = useState(null), [detail, setDetail] = useState(null), [loading, setLoading] = useState(false), [reading, setReading] = useState(false), [busy, setBusy] = useState(false), [error, setError] = useState("");
|
|
2248
|
+
const busyRef = useRef(false), context = useRef(0);
|
|
2249
|
+
useEffect(() => {
|
|
2250
|
+
if (!document.getElementById("dshe-mailbox-css")) { const css = document.createElement("style"); css.id = "dshe-mailbox-css"; css.textContent = ".dshe-mail-toolbar{display:flex;flex-wrap:wrap;gap:10px;align-items:end}.dshe-mail-toolbar .dshe-field{flex:1 1 160px;margin:0}.dshe-mail-search{display:flex;gap:8px;flex:1 1 260px;align-items:end}.dshe-mail-search input[type=search]{min-width:0;width:100%}.dshe-mail-search>.dshe-field{flex:1 1 180px;min-width:120px}.dshe-mail-search .dshe-check{flex:0 0 auto;white-space:nowrap;display:flex;align-items:center;gap:6px}.dshe-mail-search input[type=checkbox]{width:auto}.dshe-mail-grid{display:grid;grid-template-columns:minmax(220px,1fr) minmax(0,1.5fr);gap:12px;margin-top:16px}.dshe-mail-list{display:flex;flex-direction:column;gap:6px;max-height:60vh;overflow:auto;min-width:0}.dshe-mail-row{display:block;width:100%;text-align:left;padding:12px;border-radius:10px;border:1px solid rgba(128,128,128,.3);background:transparent;color:inherit;cursor:pointer}.dshe-mail-row[aria-pressed=true]{background:rgba(80,130,220,.13);outline:2px solid rgba(80,130,220,.45);outline-offset:-2px}.dshe-mail-row strong,.dshe-mail-row small{display:block;overflow-wrap:anywhere}.dshe-mail-row small{opacity:.75;margin-top:5px}.dshe-mail-detail{min-width:0;padding:14px;border:1px solid rgba(128,128,128,.3);border-radius:12px}.dshe-mail-detail h3,.dshe-mail-detail p{overflow-wrap:anywhere}.dshe-mail-detail pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:55vh;overflow:auto;font:inherit;line-height:1.7;border-top:1px solid rgba(128,128,128,.25);padding-top:12px}.dshe-mail-meta{font-size:12px;opacity:.8}.dshe-mail-footer{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:12px}.dshe-mail-empty{padding:24px 10px;opacity:.75}.dshe-tabs{flex-wrap:wrap}@media(max-width:760px){.dshe-mail-grid{grid-template-columns:minmax(0,1fr)}.dshe-mail-list{max-height:35vh}.dshe-mail-detail pre{max-height:none}.dshe-mail-search{flex-wrap:wrap}}"; document.head.appendChild(css); }
|
|
2251
|
+
}, []);
|
|
2252
|
+
useEffect(() => {
|
|
2253
|
+
if (!active) return;
|
|
2254
|
+
const ctrl = new AbortController(); setError("");
|
|
2255
|
+
mailboxApi("accounts", {}, ctrl.signal).then(value => { if(ctrl.signal.aborted)return;setAccounts(value.accounts);setAccount(old => value.accounts.some(a => a.name === old) ? old : value.defaultAccount); }).catch(e => {if(!ctrl.signal.aborted)setError(emailError(e));});
|
|
2256
|
+
return () => ctrl.abort();
|
|
2257
|
+
}, [active]);
|
|
2258
|
+
useEffect(() => {
|
|
2259
|
+
context.current++; setSelected(null); setDetail(null); setResult(null); setFolder(""); setFolders([]); setOffset(0); setScanOffset(0);
|
|
2260
|
+
if (!active || !account) return;
|
|
2261
|
+
const ctrl = new AbortController();
|
|
2262
|
+
mailboxApi("folders", {account}, ctrl.signal).then(value => { if(ctrl.signal.aborted)return;setFolders(value.folders);setFolder(value.folders.find(f => f.specialUse === "\\Inbox")?.path || value.folders.find(f => f.path === "INBOX")?.path || value.folders[0]?.path || ""); }).catch(e=>{if(!ctrl.signal.aborted)setError(emailError(e));});
|
|
2263
|
+
return () => ctrl.abort();
|
|
2264
|
+
}, [active, account]);
|
|
2265
|
+
useEffect(() => {
|
|
2266
|
+
context.current++; setSelected(null); setDetail(null); setResult(null);
|
|
2267
|
+
if(!active || !account || !folder)return;
|
|
2268
|
+
const ctrl=new AbortController();setLoading(true);setError("");
|
|
2269
|
+
mailboxApi("list", {account,folder,query,offset,unreadOnly:unread,headerScanOffset:scanOffset}, ctrl.signal).then(value=>{if(!ctrl.signal.aborted)setResult(value);}).catch(e=>{if(!ctrl.signal.aborted)setError(emailError(e));}).finally(()=>{if(!ctrl.signal.aborted)setLoading(false);});
|
|
2270
|
+
return ()=>ctrl.abort();
|
|
2271
|
+
},[active,account,folder,query,offset,scanOffset,unread,refresh]);
|
|
2272
|
+
useEffect(()=>{
|
|
2273
|
+
setDetail(null); if(!active || selected===null)return;
|
|
2274
|
+
const ctrl=new AbortController();setReading(true);setError("");
|
|
2275
|
+
mailboxApi("read",{account,folder,uid:selected},ctrl.signal).then(value=>{if(!ctrl.signal.aborted)setDetail(value);}).catch(e=>{if(!ctrl.signal.aborted)setError(emailError(e));}).finally(()=>{if(!ctrl.signal.aborted)setReading(false);});
|
|
2276
|
+
return ()=>ctrl.abort();
|
|
2277
|
+
},[active,account,folder,selected]);
|
|
2278
|
+
const run=async task=>{if(busyRef.current)return;busyRef.current=true;setBusy(true);setError("");const generation=context.current;try{await task(()=>generation===context.current);}catch(e){if(generation===context.current)setError(emailError(e));}finally{busyRef.current=false;setBusy(false);}};
|
|
2279
|
+
const reset=()=>{setOffset(0);setScanOffset(0);setSelected(null);setDetail(null);};
|
|
2280
|
+
const changeFolder=value=>{reset();setFolder(value);};
|
|
2281
|
+
const who=list=>(list||[]).map(a=>a.name&&a.address?a.name+" <"+a.address+">":a.address||a.name).filter(Boolean).join(", ");
|
|
2282
|
+
const date=value=>{const d=new Date(value);return Number.isNaN(d.getTime())?value:d.toLocaleString(UI_LANG==="en"?"en-US":"zh-CN");};
|
|
2283
|
+
const rows=result?.messages||[], currentRow=rows.find(m=>m.uid===selected), hasMore=result?.hasMore??(result?.headerScan&&result?.searchSource==="local-headers"?offset+rows.length<result.count:result?.bodyScan?rows.length===20:offset+rows.length<(result?.count||0));
|
|
2284
|
+
const folderButton=(special,zh,en)=>{const target=folders.find(f=>f.specialUse===special)||folders.find(f=>special==="\\Sent"?/^(sent|sent messages|已发送)$/i.test(f.path):f.path==="INBOX");return target?h("button",{type:"button",className:"dshe-btn",disabled:busy||loading,"aria-pressed":folder===target.path,onClick:()=>changeFolder(target.path)},dt(zh,en)):null;};
|
|
2285
|
+
return h("section",{className:"dshe-panel","aria-label":dt("邮箱工作台","Mailbox workbench")},[
|
|
2286
|
+
h("div",{className:"dshe-mail-toolbar"},[
|
|
2287
|
+
h("div",{className:"dshe-field"},[h("label",{htmlFor:"dshe-mail-account"},dt("邮箱账号","Account")),h("select",{id:"dshe-mail-account",value:account,disabled:busy,onChange:e=>{reset();setInput("");setQuery("");setAccount(e.target.value);}},accounts.map(a=>h("option",{key:a.name,value:a.name},a.name+" · "+a.user)))]),
|
|
2288
|
+
h("div",{className:"dshe-field"},[h("label",{htmlFor:"dshe-mail-folder"},dt("文件夹","Folder")),h("select",{id:"dshe-mail-folder",value:folder,disabled:busy||!folders.length,onChange:e=>changeFolder(e.target.value)},folders.map(f=>h("option",{key:f.path,value:f.path},f.name)))]),
|
|
2289
|
+
h("button",{type:"button",className:"dshe-btn primary",disabled:busy||!account,onClick:()=>run(async valid=>{const draft=await draftApi("create",{account,to:"",cc:"",subject:"",text:""});if(valid())onDraft(draft.id);})},dt("写邮件","Compose")),
|
|
2290
|
+
h("button",{type:"button",className:"dshe-btn",disabled:loading||busy||!account,onClick:()=>setRefresh(n=>n+1)},dt("刷新邮件","Refresh mail")),
|
|
2291
|
+
]),
|
|
2292
|
+
h("form",{className:"dshe-mail-search",onSubmit:e=>{e.preventDefault();reset();setUnread(false);setQuery(input.trim());setRefresh(n=>n+1);}},[
|
|
2293
|
+
h("div",{className:"dshe-field"},[h("label",{htmlFor:"dshe-mail-search"},dt("搜索当前文件夹","Search current folder")),h("input",{id:"dshe-mail-search",type:"search",value:input,maxLength:500,disabled:busy,onChange:e=>setInput(e.target.value)})]),
|
|
2294
|
+
h("button",{type:"submit",className:"dshe-btn",disabled:loading||busy||!folder},dt("搜索","Search")),
|
|
2295
|
+
query?h("button",{type:"button",className:"dshe-btn",disabled:busy,onClick:()=>{reset();setInput("");setQuery("");}},dt("清除搜索","Clear search")):null,
|
|
2296
|
+
h("label",{className:"dshe-check"},[h("input",{type:"checkbox",checked:unread,disabled:busy||!!query,onChange:e=>{reset();setUnread(e.target.checked);}}),dt("只看未读","Unread only")]),
|
|
2297
|
+
]),
|
|
2298
|
+
h("div",{className:"dshe-actions"},[folderButton("\\Inbox","收件箱","Inbox"),folderButton("\\Sent","已发送","Sent")]),
|
|
2299
|
+
error?h("div",{className:"dshe-alert error",role:"alert"},[error," ",h("button",{type:"button",className:"dshe-btn",onClick:onConfigure},dt("检查账号设置","Check account settings"))]):null,
|
|
2300
|
+
h("div",{role:"status","aria-live":"polite",className:"dshe-hint"},loading?dt("正在读取邮件…","Loading messages…"):result?dt("本页 ","This page: ")+rows.length+dt(" 封"," messages")+(result.order==="relevance"?dt(" · 按相关性排序"," · Ranked by relevance"):dt(" · 按邮箱到达序排列"," · Mailbox arrival order")):dt("在账号设置中添加并测试邮箱后,即可浏览邮件。","Add and test an account in settings to browse mail.")),
|
|
2301
|
+
result?.headerScan?h("p",{className:"dshe-hint"},dt("信封实际扫描:","Headers scanned: ")+result.headerScan.scanned+" / "+result.headerScan.mailboxTotal+(result.headerScan.complete?dt(",信封覆盖完整;正文另有限制。","; headers fully covered; body search has a separate limit."):dt(",尚未覆盖全文件夹。","; folder coverage is partial."))):null,
|
|
2302
|
+
result?.searchSource==="api"?h("p",{className:"dshe-hint"},dt("AgentMail 搜索由服务按相关性返回,垃圾箱等特殊邮件不在其搜索范围。","AgentMail ranks results by relevance and excludes spam, trash and other protected mail.")):null,
|
|
2303
|
+
h("div",{className:"dshe-mail-grid"},[
|
|
2304
|
+
h("div",{className:"dshe-mail-list","aria-label":dt("邮件列表","Message list")},rows.length?rows.map(m=>h("button",{key:String(m.uid),type:"button",className:"dshe-mail-row",disabled:busy,"aria-pressed":selected===m.uid,onClick:()=>setSelected(m.uid)},[h("strong",null,(m.flagged?"★ ":"")+(m.seen?"":"● ")+(m.subject||dt("(无主题)","(No subject)"))),h("small",null,who(m.from)),h("small",null,date(m.date))])):h("p",{className:"dshe-mail-empty"},loading?dt("请稍候…","Please wait…"):query?dt("当前检索范围内没有结果。","No results in the current search window."):dt("当前页没有邮件。","No messages on this page."))),
|
|
2305
|
+
h("article",{className:"dshe-mail-detail","aria-label":dt("邮件详情","Message details")},reading&&selected!==null?h("p",{role:"status"},dt("正在读取正文…","Loading message…")):detail?[
|
|
2306
|
+
h("h3",null,detail.subject||dt("(无主题)","(No subject)")),h("p",{className:"dshe-mail-meta"},dt("发件人:","From: ")+who(detail.from)),h("p",{className:"dshe-mail-meta"},dt("收件人:","To: ")+who(detail.to)),detail.cc?.length?h("p",{className:"dshe-mail-meta"},"Cc: "+who(detail.cc)):null,h("p",{className:"dshe-mail-meta"},date(detail.date)),
|
|
2307
|
+
h("div",{className:"dshe-actions"},[
|
|
2308
|
+
...[false,true].map(all=>h("button",{key:String(all),type:"button",className:"dshe-btn",disabled:busy,onClick:()=>run(async valid=>{const draft=await mailboxApi("replyDraft",{account,folder,uid:selected,replyAll:all});if(valid())onDraft(draft.id);})},all?dt("回复全部到草稿","Reply all in drafts"):dt("回复到草稿","Reply in drafts"))),
|
|
2309
|
+
h("button",{type:"button",className:"dshe-btn",disabled:busy,onClick:()=>run(async valid=>{await mailboxApi("mark",{account,folder,uid:selected,mark:currentRow?.seen?"unread":"read"});if(valid())setResult(old=>({...old,messages:old.messages.map(m=>m.uid===selected?{...m,seen:!m.seen}:m)}));})},currentRow?.seen?dt("标记未读","Mark unread"):dt("标记已读","Mark read")),
|
|
2310
|
+
h("button",{type:"button",className:"dshe-btn",disabled:busy,onClick:()=>run(async valid=>{await mailboxApi("mark",{account,folder,uid:selected,mark:currentRow?.flagged?"unstar":"star"});if(valid())setResult(old=>({...old,messages:old.messages.map(m=>m.uid===selected?{...m,flagged:!m.flagged}:m)}));})},currentRow?.flagged?dt("取消星标","Unstar"):dt("添加星标","Star")),
|
|
2311
|
+
]),
|
|
2312
|
+
h("pre",null,detail.text||dt("(空正文)","(Empty body)")),detail.truncated?h("p",{className:"dshe-hint"},dt("正文超过显示上限,已截断。","Body truncated at the display limit.")):null,
|
|
2313
|
+
detail.attachments?.length?h("ul",{className:"dshe-files"},detail.attachments.map((file,index)=>h("li",{key:index},[h("span",null,file.filename+" · "+Math.ceil(file.size/1024)+" KiB"),h("button",{type:"button",className:"dshe-btn",disabled:busy,onClick:()=>run(async valid=>{const blob=await mailboxApi("download",{account,folder,uid:selected,index});if(!valid())return;const url=URL.createObjectURL(blob),link=document.createElement("a");link.href=url;link.download=file.filename;link.click();setTimeout(()=>URL.revokeObjectURL(url),1000);})},dt("下载附件","Download attachment"))]))):null,
|
|
2314
|
+
]:h("p",{className:"dshe-mail-empty"},dt("选择一封邮件查看正文。","Select a message to read it."))),
|
|
2315
|
+
]),
|
|
2316
|
+
h("div",{className:"dshe-mail-footer"},[
|
|
2317
|
+
h("button",{type:"button",className:"dshe-btn",disabled:loading||busy||offset===0,onClick:()=>setOffset(n=>Math.max(0,n-20))},dt("上一页","Previous page")),h("span",null,dt("第 ","Page ")+(Math.floor(offset/20)+1)+dt(" 页","")),h("button",{type:"button",className:"dshe-btn",disabled:loading||busy||!hasMore,onClick:()=>setOffset(n=>n+20)},dt("下一页","Next page")),
|
|
2318
|
+
result?.headerScan?.nextScanOffset!==undefined?h("button",{type:"button",className:"dshe-btn",disabled:loading||busy,onClick:()=>{setOffset(0);setScanOffset(result.headerScan.nextScanOffset);}},dt("继续搜索更早邮件","Search older headers")):null,
|
|
2319
|
+
]),
|
|
2320
|
+
]);
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2231
2323
|
const DRAFT_ROUTE = "api/dsh-email/drafts";
|
|
2232
2324
|
const dt = (zh, en) => UI_LANG === "en" ? en : zh;
|
|
2233
2325
|
// Session memory survives closing the host settings dialog. A page reload
|
|
@@ -2244,15 +2336,17 @@ async function draftApi(action, payload, upload) {
|
|
|
2244
2336
|
return result.value;
|
|
2245
2337
|
}
|
|
2246
2338
|
const draftForm = (draft) => ({ account: draft.account || "", to: draft.to || "", cc: draft.cc || "", subject: draft.subject || "", text: draft.text || "", attachments: draft.attachments.map(file => file.id) });
|
|
2247
|
-
const draftState = (state) => ({ draft: dt("未发送", "Not sent"), sending: dt("正在提交 / 待核对", "Submitting / check mailbox"), sent: dt("
|
|
2339
|
+
const draftState = (state) => ({ draft: dt("未发送", "Not sent"), sending: dt("正在提交 / 待核对", "Submitting / check mailbox"), sent: dt("已保存服务端回执", "Server receipt saved"), uncertain: dt("结果未确定,请核对邮箱", "Outcome uncertain, check mailbox") })[state] || state;
|
|
2248
2340
|
|
|
2249
|
-
function DraftWorkbench({ active }) {
|
|
2341
|
+
function DraftWorkbench({ active, requestedDraft }) {
|
|
2250
2342
|
const [data, setData] = useState({ drafts: [], accounts: [] }), [selected, setSelected] = useState(() => draftUiCache && draftUiCache.selected), [form, setForm] = useState(() => draftUiCache && draftUiCache.form);
|
|
2251
2343
|
const [busy, setBusy] = useState(false), [error, setError] = useState(""), [notice, setNotice] = useState(""), [preview, setPreview] = useState(null), [acknowledged, setAcknowledged] = useState(false), [pending, setPending] = useState(null), [remove, setRemove] = useState(false), [copy, setCopy] = useState(false);
|
|
2252
|
-
const operation = useRef(false);
|
|
2344
|
+
const operation = useRef(false), handledRequested = useRef("");
|
|
2253
2345
|
const dirty = selected && form && JSON.stringify(form) !== JSON.stringify(draftForm(selected));
|
|
2254
2346
|
useEffect(() => { draftUiCache = { selected, form }; }, [selected, form]);
|
|
2255
2347
|
const locked = !selected || selected.state !== "draft";
|
|
2348
|
+
const attachmentCap = data.attachmentLimits?.[form?.account] ?? 20 * 1024 * 1024;
|
|
2349
|
+
const capLabel = attachmentCap >= 1024 * 1024 ? (attachmentCap / 1024 / 1024) + " MiB" : Math.ceil(attachmentCap / 1024) + " KiB";
|
|
2256
2350
|
const reload = async () => { const next = await draftApi("list"); setData(next); return next; };
|
|
2257
2351
|
const run = async (fn) => {
|
|
2258
2352
|
if (operation.current) return;
|
|
@@ -2272,9 +2366,10 @@ function DraftWorkbench({ active }) {
|
|
|
2272
2366
|
await reload(); setPending(null);
|
|
2273
2367
|
};
|
|
2274
2368
|
const requestSwitch = (target) => { if (dirty) setPending(target); else run(() => switchTo(target)); };
|
|
2369
|
+
useEffect(() => { if(active && requestedDraft && handledRequested.current!==requestedDraft && !operation.current && !busy) {handledRequested.current=requestedDraft;requestSwitch({kind:"open",id:requestedDraft});} }, [active,requestedDraft,busy]);
|
|
2275
2370
|
const field = (key, zh, en, multiline) => h("div", { className: "dshe-field", key }, [
|
|
2276
2371
|
h("label", { htmlFor: "dshe-draft-" + key }, dt(zh, en)),
|
|
2277
|
-
h(multiline ? "textarea" : "input", { id: "dshe-draft-" + key, type: multiline ? undefined : "text", value: form[key], disabled: busy || locked, rows: multiline ? 8 : undefined, maxLength: multiline ? 102400 : key === "subject" ? 500 : 4096, onChange: (event) => { const value = event.target.value; setForm(current => ({ ...current, [key]: value })); setPreview(null); setAcknowledged(false); setNotice(""); } }),
|
|
2372
|
+
h(multiline ? "textarea" : "input", { id: "dshe-draft-" + key, type: multiline ? undefined : "text", value: form[key], disabled: busy || locked || !!selected?.reply && key === "subject", rows: multiline ? 8 : undefined, maxLength: multiline ? 102400 : key === "subject" ? 500 : 4096, onChange: (event) => { const value = event.target.value; setForm(current => ({ ...current, [key]: value })); setPreview(null); setAcknowledged(false); setNotice(""); } }),
|
|
2278
2373
|
]);
|
|
2279
2374
|
const review = preview && h("section", { className: "dshe-review", "aria-label": dt("发送前预览", "Review before sending") }, [
|
|
2280
2375
|
h("h3", null, dt("发送前预览", "Review before sending")),
|
|
@@ -2314,16 +2409,16 @@ function DraftWorkbench({ active }) {
|
|
|
2314
2409
|
h("aside", { className: "dshe-draft-list", "aria-label": dt("已保存的草稿", "Saved drafts") }, data.drafts.length ? data.drafts.map(draft => h("button", { key: draft.id, type: "button", className: "dshe-draft-row", disabled: busy, "aria-pressed": selected && selected.id === draft.id, onClick: () => requestSwitch({ kind: "open", id: draft.id }) }, [h("strong", null, draft.subject || dt("未命名草稿", "Untitled draft")), h("small", null, draft.account + " · " + draftState(draft.state))])) : h("p", { className: "dshe-hint" }, dt("还没有草稿,先新建一份。", "No drafts yet. Create your first draft."))),
|
|
2315
2410
|
selected && form ? h("div", { className: "dshe-draft-editor" }, [
|
|
2316
2411
|
h("div", { role: "status", className: "dshe-hint" }, dirty ? dt("有未保存的编辑", "Unsaved edits") : draftState(selected.state)),
|
|
2317
|
-
h("div", { className: "dshe-field" }, [h("label", { htmlFor: "dshe-draft-account" }, dt("发件账号", "Sending account")), h("select", { id: "dshe-draft-account", value: form.account, disabled: busy || locked, onChange: event => { const value = event.target.value; setForm(current => ({ ...current, account: value })); setPreview(null); setAcknowledged(false); } }, [h("option", { value: "" }, dt("请选择账号", "Choose an account")), ...[...new Set([...data.accounts, ...(form.account ? [form.account] : [])])].map(name => h("option", { key: name, value: name }, name))])]),
|
|
2412
|
+
h("div", { className: "dshe-field" }, [h("label", { htmlFor: "dshe-draft-account" }, dt("发件账号", "Sending account")), h("select", { id: "dshe-draft-account", value: form.account, disabled: busy || locked || !!selected?.reply, onChange: event => { const value = event.target.value; setForm(current => ({ ...current, account: value })); setPreview(null); setAcknowledged(false); } }, [h("option", { value: "" }, dt("请选择账号", "Choose an account")), ...[...new Set([...data.accounts, ...(form.account ? [form.account] : [])])].map(name => h("option", { key: name, value: name }, name))])]),
|
|
2318
2413
|
field("to", "收件人 To", "Recipients To"), field("cc", "抄送 Cc", "Copy Cc"), field("subject", "主题", "Subject"), field("text", "正文", "Message", true),
|
|
2319
2414
|
h("ul", { className: "dshe-files" }, selected.attachments.filter(file => form.attachments.includes(file.id)).map(file => h("li", { key: file.id }, [h("span", null, file.filename), !locked ? h("button", { type: "button", className: "dshe-btn", disabled: busy, "aria-label": dt("移除附件 ", "Remove attachment ") + file.filename, onClick: () => { setForm(current => ({ ...current, attachments: current.attachments.filter(id => id !== file.id) })); setPreview(null); setAcknowledged(false); } }, dt("移除", "Remove")) : null]))),
|
|
2320
|
-
!locked ? h("label", { className: "dshe-upload" }, [dt("添加附件(先保存编辑,最多 10 个 / 合计
|
|
2415
|
+
!locked ? h("label", { className: "dshe-upload" }, [dt("添加附件(先保存编辑,最多 10 个 / 合计 ", "Add attachments (save edits first; up to 10 files / ") + capLabel + dt(")", " total)"), h("input", { type: "file", multiple: true, disabled: busy || dirty, "aria-label": dt("添加附件", "Add attachments"), onChange: event => { const files = Array.from(event.target.files || []); event.target.value = ""; run(async () => { let current = selected; for (const file of files) { if (file.size > attachmentCap) throw new Error(dt("单个附件超过 ", "Attachment exceeds ") + capLabel); current = await draftApi("upload", { id: current.id, revision: current.revision }, file); adopt(current); } await reload(); setNotice(dt("附件已添加,尚未发送。请检查预览。", "Attachments added. Review before sending.")); }); } })]) : null,
|
|
2321
2416
|
!locked ? h("div", { className: "dshe-actions" }, [
|
|
2322
2417
|
h("button", { type: "button", className: "dshe-btn", disabled: busy || !dirty, onClick: () => run(save) }, dt("保存草稿", "Save draft")),
|
|
2323
2418
|
h("button", { type: "button", className: "dshe-btn primary", disabled: busy || dirty, onClick: () => run(async () => { setPreview(await draftApi("preview", { id: selected.id, revision: selected.revision })); setAcknowledged(false); setNotice(""); }) }, dt("检查发送预览", "Review before sending")),
|
|
2324
2419
|
]) : null,
|
|
2325
2420
|
review,
|
|
2326
|
-
selected.receipt ? h("div", { className: "dshe-alert " + (selected.receipt.rejected.length ? "info" : "success"), role: "status" }, [(selected.receipt.rejected.length ? dt("部分地址被服务器拒绝,邮件不会自动重发。服务器接受:", "Some addresses were rejected. This email will not retry automatically. Server accepted: ") : dt("
|
|
2421
|
+
selected.receipt ? h("div", { className: "dshe-alert " + (selected.receipt.rejected.length ? "info" : "success"), role: "status" }, [(selected.receipt.rejected.length ? dt("部分地址被服务器拒绝,邮件不会自动重发。服务器接受:", "Some addresses were rejected. This email will not retry automatically. Server accepted: ") : dt("服务器已返回回执,邮件不会再次发送。服务器接受:", "Server receipt saved. This draft will not send again. Server accepted: ")) + (selected.receipt.accepted.join(", ") || dt("无", "none")), h("p", null, dt("服务器拒绝:", "Server rejected: ") + (selected.receipt.rejected.join(", ") || dt("无", "none"))), h("p", null, dt("这是服务器提交结果,不代表已送达收件人的收件箱。", "This confirms the server response, not delivery to the recipient inbox."))]) : null,
|
|
2327
2422
|
selected.state === "uncertain" || selected.state === "sending" ? h("div", { className: "dshe-alert info", role: "status" }, selected.error || dt("提交尚未留下回执。先核对邮箱,再刷新列表;不要重复发送。", "No receipt yet. Check your mailbox, then refresh the list. Do not send again.")) : null,
|
|
2328
2423
|
h("div", { className: "dshe-local-actions" }, [
|
|
2329
2424
|
h("button", { type: "button", className: "dshe-btn", disabled: busy || selected.state === "sending" || dirty, onClick: () => setCopy(true) }, dt("复制为新草稿", "Copy to a new draft")),
|
|
@@ -2374,6 +2469,8 @@ function RecipientRules({ active, onSaved }) {
|
|
|
2374
2469
|
|
|
2375
2470
|
function EmailSettingsSection() {
|
|
2376
2471
|
const [tab, setTab] = useState(emailActiveTab);
|
|
2472
|
+
const [requestedDraft,setRequestedDraft]=useState("");
|
|
2473
|
+
const openMailboxDraft=id=>{setRequestedDraft(id);setTab("drafts");};
|
|
2377
2474
|
useEffect(() => { emailActiveTab = tab; }, [tab]);
|
|
2378
2475
|
const [draft, setDraft] = useState(null);
|
|
2379
2476
|
useEffect(() => {
|
|
@@ -2661,13 +2758,14 @@ function EmailSettingsSection() {
|
|
|
2661
2758
|
|
|
2662
2759
|
return h("div", { className: "dshe-settings" }, [
|
|
2663
2760
|
h("header", { className: "dshe-header" }, [
|
|
2664
|
-
h("span", { className: "dshe-kicker" }, "dsh-email · IMAP/SMTP"),
|
|
2761
|
+
h("span", { className: "dshe-kicker" }, "dsh-email · IMAP / SMTP / AgentMail"),
|
|
2665
2762
|
h("h2", null, t("section.title")),
|
|
2666
|
-
h("p", null, dt("
|
|
2763
|
+
h("p", null, dt("浏览和搜索邮件,把回复存成草稿,核对后发送。账号设置修改后会自动保存。", "Browse and search mail, prepare replies in drafts, and review before sending. Account settings save automatically.")),
|
|
2667
2764
|
]),
|
|
2668
2765
|
tab === "accounts" ? status : null,
|
|
2669
|
-
h("div", { className: "dshe-tabs", role: "tablist", "aria-label": dt("邮件功能", "Email features") }, [["accounts", dt("邮箱账号", "Accounts")], ["drafts", dt("草稿", "Drafts")], ["rules", dt("收件人规则", "Recipient rules")]].map(([id, label]) => h("button", { key: id, type: "button", role: "tab", className: "dshe-btn", "aria-selected": tab === id, "aria-controls": "dshe-pane-" + id, onClick: () => setTab(id) }, label))),
|
|
2670
|
-
h("div", {
|
|
2766
|
+
h("div", { className: "dshe-tabs", role: "tablist", "aria-label": dt("邮件功能", "Email features") }, [["accounts", dt("邮箱账号", "Accounts")], ["mailbox", dt("收发邮件", "Mailbox")], ["drafts", dt("草稿", "Drafts")], ["rules", dt("收件人规则", "Recipient rules")]].map(([id, label]) => h("button", { key: id, type: "button", role: "tab", className: "dshe-btn", "aria-selected": tab === id, "aria-controls": "dshe-pane-" + id, onClick: () => setTab(id) }, label))),
|
|
2767
|
+
h("div", {id:"dshe-pane-mailbox",role:"tabpanel",hidden:tab!=="mailbox",className:"dshe-section-hidden"},h(MailboxWorkbench,{active:tab==="mailbox",onConfigure:()=>setTab("accounts"),onDraft:openMailboxDraft})),
|
|
2768
|
+
h("div", { id: "dshe-pane-drafts", role: "tabpanel", hidden: tab !== "drafts", className: "dshe-section-hidden" }, h(DraftWorkbench, { active: tab === "drafts", requestedDraft })),
|
|
2671
2769
|
h("div", { id: "dshe-pane-rules", role: "tabpanel", hidden: tab !== "rules", className: "dshe-section-hidden" }, h(RecipientRules, { active: tab === "rules", onSaved: revision => { setSnapshot(old => ({ ...old, settings: { ...old.settings, revision } })); } })),
|
|
2672
2770
|
h("section", { id: "dshe-pane-accounts", role: "tabpanel", hidden: tab !== "accounts", className: "dshe-panel dshe-section-hidden" }, [
|
|
2673
2771
|
// 全局设置:发信确认和下载目录对所有账号一视同仁,不属于任何一张卡片,
|
package/lib/config.d.ts
CHANGED
|
@@ -71,6 +71,8 @@ export declare const ENDPOINT_DEFAULTS: {
|
|
|
71
71
|
export declare function normalizeSettingsEndpoint<T extends SmtpConfig>(value: T, kind: keyof typeof ENDPOINT_DEFAULTS): Omit<T, 'host' | 'port' | 'secure'> & SmtpConfig;
|
|
72
72
|
/** One mailbox account. Top-level shorthand fields act as shared defaults. */
|
|
73
73
|
export interface AccountConfig {
|
|
74
|
+
/** AgentMail credential; the account editor also accepts it through its secret field. */
|
|
75
|
+
apiKey?: string;
|
|
74
76
|
/** Built-in provider name, or a custom serverPresets name. */
|
|
75
77
|
provider?: ProviderRef;
|
|
76
78
|
user?: string;
|
|
@@ -175,6 +177,8 @@ export declare const PROVIDER_NAMES: string[];
|
|
|
175
177
|
export declare const EMAIL_PASSWORD_ENV = "DSH_EMAIL_PASSWORD";
|
|
176
178
|
/** Fully resolved, validated configuration for one account. */
|
|
177
179
|
export interface ResolvedEmailConfig {
|
|
180
|
+
transport?: 'agentmail';
|
|
181
|
+
apiBase?: 'https://api.agentmail.to' | 'https://api.agentmail.eu';
|
|
178
182
|
user: string;
|
|
179
183
|
/** Display name for the From header, '' when the account does not set one. */
|
|
180
184
|
senderName: string;
|
package/lib/config.js
CHANGED
|
@@ -228,7 +228,7 @@ export function resolveEmailSettings(config, onlyAccount) {
|
|
|
228
228
|
const common = {
|
|
229
229
|
provider: raw.provider,
|
|
230
230
|
user: raw.user,
|
|
231
|
-
password: raw.password,
|
|
231
|
+
password: raw.password, apiKey: raw.apiKey,
|
|
232
232
|
senderName: raw.senderName,
|
|
233
233
|
authUser: raw.authUser,
|
|
234
234
|
authPassword: raw.authPassword,
|
|
@@ -327,7 +327,7 @@ function providerTable(custom) {
|
|
|
327
327
|
}
|
|
328
328
|
/** Every name a `provider:` may legally use, built-ins first. */
|
|
329
329
|
export function providerNames(custom = {}) {
|
|
330
|
-
const names = [...PROVIDER_NAMES];
|
|
330
|
+
const names = [...PROVIDER_NAMES, 'agentmail', 'agentmail-eu'];
|
|
331
331
|
for (const name of Object.keys(custom))
|
|
332
332
|
if (!names.includes(name))
|
|
333
333
|
names.push(name);
|
|
@@ -369,6 +369,16 @@ function resolveAccount(name, common, acc, allowEnvPassword, providers, known) {
|
|
|
369
369
|
// Normalize it like the legacy projection and YAML writer before preset lookup.
|
|
370
370
|
const requested = (acc.provider === '' ? undefined : acc.provider) ?? (common.provider === '' ? undefined : common.provider);
|
|
371
371
|
const preset = requested === undefined ? undefined : providers[requested];
|
|
372
|
+
if (requested === 'agentmail' || requested === 'agentmail-eu') {
|
|
373
|
+
const user = String(acc.user ?? common.user ?? '').trim();
|
|
374
|
+
const sharedApi = common.provider === requested;
|
|
375
|
+
const key = String(acc.password || acc.apiKey || (sharedApi ? common.password || common.apiKey : '') || (allowEnvPassword ? process.env.DSH_AGENTMAIL_API_KEY ?? '' : '')).trim();
|
|
376
|
+
if (!/^[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+$/.test(user) || !key || key.length > 4096 || /[^\x21-\x7e]/.test(key))
|
|
377
|
+
throw new Error('AgentMail 账号需要有效邮箱地址和 API Key,请在「设置 → 邮件」补全后测试连接。');
|
|
378
|
+
return { transport: 'agentmail', apiBase: requested === 'agentmail-eu' ? 'https://api.agentmail.eu' : 'https://api.agentmail.to', user,
|
|
379
|
+
senderName: '', authUser: user, authPassword: key, password: key, authKind: 'password',
|
|
380
|
+
imap: { host: '', port: 993, secure: true }, smtp: { host: '', port: 465, secure: true }, inboxFolder: 'INBOX' };
|
|
381
|
+
}
|
|
372
382
|
if (requested !== undefined && preset === undefined) {
|
|
373
383
|
throw new Error(`dsh-email:账号 "${name}" 的 provider "${requested}" 未知,可选:${known.join('/')};或省略 provider 直接填 imap.host 与 smtp.host`);
|
|
374
384
|
}
|
package/lib/draft-store.d.ts
CHANGED
package/lib/draft-store.js
CHANGED
|
@@ -43,6 +43,10 @@ export class DraftStore {
|
|
|
43
43
|
if (!isId(draft.id) || !Number.isSafeInteger(draft.revision) || draft.revision < 1 || !['draft', 'sending', 'sent', 'uncertain'].includes(draft.state))
|
|
44
44
|
throw new Error('invalid draft');
|
|
45
45
|
draftFields(draft);
|
|
46
|
+
if (draft.reply && (!(typeof draft.reply.uid === 'number' && Number.isSafeInteger(draft.reply.uid) && draft.reply.uid > 0 || typeof draft.reply.uid === 'string' && draft.reply.uid.length > 0 && draft.reply.uid.length <= 998 && !/[\r\n\0]/.test(draft.reply.uid)) || typeof draft.reply.folder !== 'string' || draft.reply.folder.length > 1024))
|
|
47
|
+
throw new Error('invalid reply reference');
|
|
48
|
+
if (draft.reply?.uidValidity !== undefined && (!Number.isSafeInteger(draft.reply.uidValidity) || draft.reply.uidValidity <= 0))
|
|
49
|
+
throw new Error('invalid reply UIDVALIDITY');
|
|
46
50
|
if (!Array.isArray(draft.attachments) || draft.attachments.length > 10 || draft.attachments.some(file => !isId(file.id) || !isAbsolute(file.path) || typeof file.filename !== 'string' || typeof file.owned !== 'boolean'))
|
|
47
51
|
throw new Error('invalid attachments');
|
|
48
52
|
}
|
package/lib/draft-web.js
CHANGED
|
@@ -37,7 +37,7 @@ export class EmailDraftBackend {
|
|
|
37
37
|
if (!config)
|
|
38
38
|
throw new DraftError('发件账号不存在,请重新选择账号');
|
|
39
39
|
const value = this.runtime.getSettingsValue(), policies = parseRecipientPolicies(value.trustedRecipientsYaml);
|
|
40
|
-
return { name, config, cap: Math.min(settings.maxAttachmentBytes, MAX_BYTES), policy: policies.get(name), key: hash({ name, config, cap: settings.maxAttachmentBytes, policies: value.trustedRecipientsYaml ?? '', approval: value.sendApproval }) };
|
|
40
|
+
return { name, config, cap: Math.min(settings.maxAttachmentBytes, config.transport === 'agentmail' ? 4 * 1024 * 1024 : MAX_BYTES), policy: policies.get(name), key: hash({ name, config, cap: settings.maxAttachmentBytes, policies: value.trustedRecipientsYaml ?? '', approval: value.sendApproval }) };
|
|
41
41
|
}
|
|
42
42
|
safe(error) {
|
|
43
43
|
let message = redactCredentials(messageOf(error, '操作失败,请稍后重试'));
|
|
@@ -127,7 +127,13 @@ export class EmailDraftBackend {
|
|
|
127
127
|
if (body.action === 'list') {
|
|
128
128
|
await this.recover(signal);
|
|
129
129
|
const snapshot = await this.settings.snapshot();
|
|
130
|
-
|
|
130
|
+
const attachmentLimits = {};
|
|
131
|
+
try {
|
|
132
|
+
for (const name of this.runtime.getEffectiveSettings().accounts.keys())
|
|
133
|
+
attachmentLimits[name] = this.connection(name).cap;
|
|
134
|
+
}
|
|
135
|
+
catch { /* Drafts remain usable before account setup. */ }
|
|
136
|
+
return { drafts: (await this.store.list()).map(view), accounts: snapshot.accounts, attachmentLimits, revision: snapshot.settings.revision, writable: snapshot.writable };
|
|
131
137
|
}
|
|
132
138
|
if (body.action === 'rules') {
|
|
133
139
|
const snapshot = await this.settings.snapshot();
|
|
@@ -162,6 +168,8 @@ export class EmailDraftBackend {
|
|
|
162
168
|
if (body.action === 'update') {
|
|
163
169
|
editable(draft);
|
|
164
170
|
const fields = draftFields(body), keep = body.attachments ?? draft.attachments.map(file => file.id);
|
|
171
|
+
if (draft.reply && (fields.account !== draft.account || fields.subject !== draft.subject))
|
|
172
|
+
throw new DraftError('回复草稿须保留原发件账号和主题;如需更换请新建邮件');
|
|
165
173
|
if (!Array.isArray(keep) || keep.some(key => !draft.attachments.some(file => file.id === key)) || new Set(keep).size !== keep.length)
|
|
166
174
|
throw new DraftError('附件列表无效,请重新打开草稿');
|
|
167
175
|
const changed = await this.store.change(id, body.revision, value => { editable(value); Object.assign(value, fields); value.attachments = value.attachments.filter(file => keep.includes(file.id)); }, signal);
|
|
@@ -192,7 +200,8 @@ export class EmailDraftBackend {
|
|
|
192
200
|
files.push(stored);
|
|
193
201
|
await this.writeUpload(owned.path, file.content, signal);
|
|
194
202
|
}
|
|
195
|
-
const changed = await this.store.change(created.id, created.revision, value => { value.attachments = files;
|
|
203
|
+
const changed = await this.store.change(created.id, created.revision, value => { value.attachments = files; if (draft.reply)
|
|
204
|
+
value.reply = { ...draft.reply }; }, signal);
|
|
196
205
|
return view(changed);
|
|
197
206
|
}
|
|
198
207
|
catch (error) {
|
|
@@ -245,7 +254,7 @@ export class EmailDraftBackend {
|
|
|
245
254
|
}
|
|
246
255
|
let receipt;
|
|
247
256
|
try {
|
|
248
|
-
receipt = await pool.sendPrepared(before.account, before.to, before.subject, before.text, before.cc, plan.attachments.map(({ filename, content }) => ({ filename, content })), current);
|
|
257
|
+
receipt = await pool.sendPrepared(before.account, before.to, before.subject, before.text, before.cc, plan.attachments.map(({ filename, content }) => ({ filename, content })), current, before.reply);
|
|
249
258
|
}
|
|
250
259
|
catch (error) {
|
|
251
260
|
if (error instanceof RecipientPolicyError) {
|
|
@@ -257,12 +266,12 @@ export class EmailDraftBackend {
|
|
|
257
266
|
throw new DraftError('提交结果未确定,且回执未能保存;请先去邮箱核对,不要直接重发', 'draft-storage');
|
|
258
267
|
return { draft: view(failed) };
|
|
259
268
|
}
|
|
260
|
-
// Once
|
|
269
|
+
// Once the server acknowledges, persist its receipt even if the browser disconnects.
|
|
261
270
|
try {
|
|
262
271
|
return { draft: view(await this.store.change(id, pending.revision, value => { value.state = 'sent'; value.receipt = receipt; })) };
|
|
263
272
|
}
|
|
264
273
|
catch {
|
|
265
|
-
throw new DraftError('
|
|
274
|
+
throw new DraftError('服务器已返回回执,但本地保存失败;请先核对邮箱,不要再次发送', 'draft-storage');
|
|
266
275
|
}
|
|
267
276
|
}, signal);
|
|
268
277
|
}
|
package/lib/host-config.js
CHANGED
|
@@ -7,6 +7,7 @@ const endpoint = () => z.object({
|
|
|
7
7
|
export const Config = z.object({
|
|
8
8
|
provider: z.string().volatile(), user: z.string().volatile(),
|
|
9
9
|
password: z.string().role('secret').volatile(),
|
|
10
|
+
apiKey: z.string().role('secret').volatile(),
|
|
10
11
|
senderName: z.string().volatile(), authUser: z.string().volatile(),
|
|
11
12
|
authPassword: z.string().role('secret').volatile(),
|
|
12
13
|
clientId: z.string().volatile(), authKind: z.union(['password', 'oauth2']).volatile(),
|
package/lib/index.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { EmailConfig } from './config.js';
|
|
2
|
+
import { type MailDependencies } from './mail-service.js';
|
|
2
3
|
export declare const Config: import("@deepseek-ai/schemastery").default;
|
|
3
4
|
export declare const name = "tool-email";
|
|
4
5
|
export declare const inject: string[];
|
|
5
6
|
export type Config = EmailConfig;
|
|
6
7
|
/** Compose settings/pool lifecycle, tools, browser routes and the outgoing-mail gate. */
|
|
7
8
|
export declare function apply(ctx: any, config?: Config): void;
|
|
9
|
+
/** Shared application assembly; transports can be supplied by embedders and protocol fixtures. */
|
|
10
|
+
export declare function installEmailApplication(ctx: any, config?: Config, dependencies?: MailDependencies): void;
|
|
8
11
|
export { clampInt, defaultDownloadDir, EMAIL_PASSWORD_ENV, isOAuth2Account, OUTLOOK_OAUTH2_CLIENT_ID, OUTLOOK_PROVIDER, parseAccountsYaml, parseServerPresets, presetNamesIn, providerNames, PROVIDER_NAMES, resolveEmailConfig, resolveEmailSettings, serializeAccountsYaml } from './config.js';
|
|
9
12
|
export type { AuthKind, ResolvedEmailConfig } from './config.js';
|
|
10
13
|
export { ACCESS_TOKEN_MARGIN_MS, classifyOAuthFailure, clearTokenFor, clientIdOf, DEVICE_CODE_URL, getFreshAccessToken, mapAadstsMessage, NO_CLIENT_ID_MESSAGE, NOT_LOGGED_IN_MESSAGE, oauth2StateOf, oauth2TokenFile, OAUTH2_SCOPES, OAuth2Error, pollDeviceFlow, readTokenStore, startDeviceFlow, TOKEN_URL, writeTokenStore, } from './oauth2.js';
|
|
@@ -17,3 +20,6 @@ export { EmailSettingsBackend, installEmailSettingsWeb, SETTINGS_ROUTE } from '.
|
|
|
17
20
|
export type { AccountCardData, AccountCardInput } from './web.js';
|
|
18
21
|
export { EmailDraftBackend, installEmailDrafts, EMAIL_DRAFT_ROUTE } from './draft-web.js';
|
|
19
22
|
export { DraftStore, DraftError } from './draft-store.js';
|
|
23
|
+
export { AgentMailClient } from './agentmail.js';
|
|
24
|
+
export { EmailService } from './mail-service.js';
|
|
25
|
+
export { EmailMailboxBackend, installEmailMailbox, MAILBOX_ROUTE } from './mailbox-web.js';
|
package/lib/index.js
CHANGED
|
@@ -6,11 +6,17 @@ import { liveConfig, Config as ConfigSchema } from './host-config.js';
|
|
|
6
6
|
import { installLegacySettingsImport } from './legacy-settings.js';
|
|
7
7
|
import { EmailSettingsSchema, SETTINGS_NAMESPACE } from './settings.js';
|
|
8
8
|
import { EmailDraftBackend, installEmailDrafts } from './draft-web.js';
|
|
9
|
+
import { EmailMailboxBackend, installEmailMailbox } from './mailbox-web.js';
|
|
10
|
+
import { EmailService } from './mail-service.js';
|
|
9
11
|
export const Config = ConfigSchema;
|
|
10
12
|
export const name = 'tool-email';
|
|
11
13
|
export const inject = ['settings', 'tools'];
|
|
12
14
|
/** Compose settings/pool lifecycle, tools, browser routes and the outgoing-mail gate. */
|
|
13
15
|
export function apply(ctx, config = {}) {
|
|
16
|
+
installEmailApplication(ctx, config);
|
|
17
|
+
}
|
|
18
|
+
/** Shared application assembly; transports can be supplied by embedders and protocol fixtures. */
|
|
19
|
+
export function installEmailApplication(ctx, config = {}, dependencies = {}) {
|
|
14
20
|
config = liveConfig(config);
|
|
15
21
|
installLegacySettingsImport(ctx, config, SETTINGS_NAMESPACE, 'tool-email', Object.keys(EmailSettingsSchema.dict));
|
|
16
22
|
if (typeof ctx.settings.configure === 'function' && typeof ctx.inject === 'function') {
|
|
@@ -18,12 +24,13 @@ export function apply(ctx, config = {}) {
|
|
|
18
24
|
settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber));
|
|
19
25
|
});
|
|
20
26
|
}
|
|
21
|
-
const runtime = createEmailRuntime(ctx, config);
|
|
22
|
-
const backend = new EmailSettingsBackend(ctx, runtime.settingsScope, config);
|
|
27
|
+
const runtime = createEmailRuntime(ctx, config, settings => new EmailService(settings, dependencies));
|
|
28
|
+
const backend = new EmailSettingsBackend(ctx, runtime.settingsScope, config, dependencies.fetch);
|
|
23
29
|
backend.watchImpl = runtime.watch;
|
|
24
30
|
installEmailSettingsWeb(ctx, backend);
|
|
25
31
|
const drafts = new EmailDraftBackend(runtime, backend);
|
|
26
32
|
installEmailDrafts(ctx, drafts);
|
|
33
|
+
installEmailMailbox(ctx, new EmailMailboxBackend(runtime, drafts));
|
|
27
34
|
for (const definition of buildEmailTools(runtime, drafts))
|
|
28
35
|
ctx.tools.register(definition);
|
|
29
36
|
installSendApproval(ctx, runtime);
|
|
@@ -37,3 +44,6 @@ export { parseEmailDay } from './tool-contract.js';
|
|
|
37
44
|
export { EmailSettingsBackend, installEmailSettingsWeb, SETTINGS_ROUTE } from './web.js';
|
|
38
45
|
export { EmailDraftBackend, installEmailDrafts, EMAIL_DRAFT_ROUTE } from './draft-web.js';
|
|
39
46
|
export { DraftStore, DraftError } from './draft-store.js';
|
|
47
|
+
export { AgentMailClient } from './agentmail.js';
|
|
48
|
+
export { EmailService } from './mail-service.js';
|
|
49
|
+
export { EmailMailboxBackend, installEmailMailbox, MAILBOX_ROUTE } from './mailbox-web.js';
|