weaver-e9-assistant-cli 1.0.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/dist/args.js +36 -0
- package/dist/auth-token.js +690 -0
- package/dist/auth.js +187 -0
- package/dist/blog.js +469 -0
- package/dist/cli.js +4041 -0
- package/dist/client.js +199 -0
- package/dist/config.js +124 -0
- package/dist/createcontext.js +41 -0
- package/dist/crypto.js +139 -0
- package/dist/cube.js +1633 -0
- package/dist/email.js +838 -0
- package/dist/fileutil.js +20 -0
- package/dist/kq.js +204 -0
- package/dist/links.js +34 -0
- package/dist/odoc.js +194 -0
- package/dist/pageopen.js +29 -0
- package/dist/parse.js +159 -0
- package/dist/querystate.js +38 -0
- package/dist/render.js +21 -0
- package/dist/report.js +50 -0
- package/dist/submissionplan.js +43 -0
- package/dist/submissionstate.js +81 -0
- package/dist/todoaskidea.js +54 -0
- package/dist/todobatch.js +154 -0
- package/dist/todocc.js +128 -0
- package/dist/todoforward.js +66 -0
- package/dist/todoreject.js +215 -0
- package/dist/todosubmit.js +277 -0
- package/dist/todotransfer.js +54 -0
- package/dist/util.js +25 -0
- package/dist/wfcreate.js +102 -0
- package/dist/wfform.js +194 -0
- package/dist/wfops.js +40 -0
- package/dist/wfreceiver.js +43 -0
- package/dist/wfstart.js +717 -0
- package/dist/wfsummary.js +89 -0
- package/dist/wfupload.js +78 -0
- package/dist/wfuser.js +60 -0
- package/dist/workflow.js +461 -0
- package/dist/workplan.js +394 -0
- package/package.json +25 -0
package/dist/email.js
ADDED
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
import { endpointFor } from './config.js';
|
|
2
|
+
import { createApiFromProfile } from './client.js';
|
|
3
|
+
import { pickByPaths, stripHtml, extractRows } from './parse.js';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { getCurrentUser } from './wfuser.js';
|
|
8
|
+
/**
|
|
9
|
+
* E9 邮件模块。比待办简单:列表是单个 GET(不像待办的 4 步取数链路)。
|
|
10
|
+
*
|
|
11
|
+
* 抓包确认的接口:
|
|
12
|
+
* GET /api/email/list/allList?folderid=0¤t=1&menuIds=536,53603&...
|
|
13
|
+
* folderid 文件夹 id(收件箱=0,已发送=-1,其余待抓包)
|
|
14
|
+
* current=<页码> 分页页码
|
|
15
|
+
* menuIds/menuPathIds 邮件菜单路径(随文件夹变:收件箱 536,53603 / 已发送 536,53604)
|
|
16
|
+
* subject/from/to 等 空字符串=不过滤;填值=按关键字过滤
|
|
17
|
+
* receivemail=true 收件箱类才带(已发送不带 receivemail/receivemailid)
|
|
18
|
+
* datetype=6+startdate+enddate 自定义日期区间(语义化 --date 转成它)
|
|
19
|
+
* __random__=<毫秒时间戳> 防缓存
|
|
20
|
+
*
|
|
21
|
+
* 响应实跑确认:行在 tableBean.datas,总数在 tableBean.total,列定义在 tableBean.columns。
|
|
22
|
+
* 鉴权复用 E9Api 双头机制,走当前 baseUrl(单一环境,所有功能同地址)。
|
|
23
|
+
*/
|
|
24
|
+
/** 收件箱菜单路径(待办中心是 1,13,邮件菜单不同)。自定义 folderid 时用它作默认。 */
|
|
25
|
+
export const DEFAULT_EMAIL_MENU_IDS = '536,53603';
|
|
26
|
+
/** 实跑确认的文件夹预设。menuIds 第三段随文件夹变:53603=收件箱/53604=已发送/53605=草稿箱/53607=待办/53609=标星。 */
|
|
27
|
+
export const DEFAULT_EMAIL_FOLDERS = {
|
|
28
|
+
inbox: { key: 'inbox', folderid: '0', menuIds: '536,53603', receivemail: true, receivemailid: '0', name: '收件箱' },
|
|
29
|
+
sent: { key: 'sent', folderid: '-1', menuIds: '536,53604', receivemail: false, name: '已发送' },
|
|
30
|
+
drafts: { key: 'drafts', folderid: '-2', menuIds: '536,53605', receivemail: false, name: '草稿箱' },
|
|
31
|
+
pending: { key: 'pending', folderid: '', menuIds: '536,53607', receivemail: false, selectorField: 'waitdeal', selectorValue: '1', name: '待办邮件' },
|
|
32
|
+
starred: { key: 'starred', folderid: '', menuIds: '536,53609', receivemail: false, selectorField: 'star', selectorValue: '1', name: '标星邮件' },
|
|
33
|
+
};
|
|
34
|
+
const FOLDER_ALIASES = {
|
|
35
|
+
inbox: 'inbox', '收件箱': 'inbox',
|
|
36
|
+
sent: 'sent', '已发送': 'sent', '发件箱': 'sent',
|
|
37
|
+
drafts: 'drafts', '草稿箱': 'drafts', '草稿': 'drafts',
|
|
38
|
+
pending: 'pending', '待办邮件': 'pending', '代办邮件': 'pending',
|
|
39
|
+
starred: 'starred', '标星邮件': 'starred', '标星': 'starred', '星标邮件': 'starred',
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* 解析文件夹入参:inbox/sent/收件箱/已发送 取预设;其余当自定义 folderid
|
|
43
|
+
* (menuIds 用收件箱默认、receivemail=true,菜单路径可能不准——自定义文件夹需自行抓包确认 menuIds)。
|
|
44
|
+
*/
|
|
45
|
+
export function resolveEmailFolder(input) {
|
|
46
|
+
if (!input)
|
|
47
|
+
return DEFAULT_EMAIL_FOLDERS.inbox;
|
|
48
|
+
const norm = input.trim().toLowerCase();
|
|
49
|
+
const key = FOLDER_ALIASES[norm] ?? (DEFAULT_EMAIL_FOLDERS[norm] ? norm : null);
|
|
50
|
+
if (key)
|
|
51
|
+
return DEFAULT_EMAIL_FOLDERS[key];
|
|
52
|
+
return { key: 'custom', folderid: input, menuIds: DEFAULT_EMAIL_MENU_IDS, receivemail: true, receivemailid: '0', name: `文件夹 ${input}` };
|
|
53
|
+
}
|
|
54
|
+
export async function getEmailList(cfg, opts = {}) {
|
|
55
|
+
const api = await createApiFromProfile(cfg);
|
|
56
|
+
const folder = resolveEmailFolder(opts.folder);
|
|
57
|
+
const page = opts.current ?? 1;
|
|
58
|
+
// allList 服务端固定每页条数且无视 pageSize 入参(已抓包实证:本环境 14 条/页)。
|
|
59
|
+
// 按「不认 pageSize 就跟随服务端默认」原则:直接读服务端页,天然连续不跳号。
|
|
60
|
+
const query = buildAllListQuery(cfg, { folder, current: page, subject: opts.subject, from: opts.from, to: opts.to, datetype: opts.datetype, startdate: opts.startdate, enddate: opts.enddate });
|
|
61
|
+
const r = await api.get('/api/email/list/allList', query);
|
|
62
|
+
const items = extractEmailItems(r.json);
|
|
63
|
+
const serverPageSize = Number(r.json?.tableBean?.pageSize) || 14;
|
|
64
|
+
return {
|
|
65
|
+
folderKey: folder.key,
|
|
66
|
+
folderName: folder.name ?? folder.key,
|
|
67
|
+
folderid: folder.folderid,
|
|
68
|
+
current: page,
|
|
69
|
+
pageSize: serverPageSize,
|
|
70
|
+
total: extractEmailTotal(r.json),
|
|
71
|
+
items,
|
|
72
|
+
raw: r.json,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 构造 allList 的 query:复刻抓包的全字段集。
|
|
77
|
+
* menuIds / menu_folderid / receivemail 都随文件夹预设走(收件箱带 receivemail,已发送不带)。
|
|
78
|
+
* 空过滤项留空字符串(等价不过滤,与抓包一致);
|
|
79
|
+
* datetype / startdate / enddate 只有传值时才加入(无过滤抓包里没有这些 key)。
|
|
80
|
+
*/
|
|
81
|
+
function buildAllListQuery(cfg, opts) {
|
|
82
|
+
void cfg; // menuIds 来自 folder 预设(各文件夹不同),不再共用 cfg.emailMenuIds
|
|
83
|
+
const f = opts.folder;
|
|
84
|
+
const q = {
|
|
85
|
+
folderid: f.selectorField ? '' : f.folderid,
|
|
86
|
+
subject: opts.subject ?? '',
|
|
87
|
+
from: opts.from ?? '',
|
|
88
|
+
to: opts.to ?? '',
|
|
89
|
+
mailaccountid: '',
|
|
90
|
+
isInternal: '',
|
|
91
|
+
status: '',
|
|
92
|
+
waitdeal: '',
|
|
93
|
+
star: '',
|
|
94
|
+
attachmentnumber: '',
|
|
95
|
+
classification: '',
|
|
96
|
+
recallState: '',
|
|
97
|
+
labelid: '',
|
|
98
|
+
current: String(opts.current),
|
|
99
|
+
pageSize: String(opts.pageSize ?? '10'),
|
|
100
|
+
menuIds: f.menuIds,
|
|
101
|
+
menuPathIds: f.menuIds,
|
|
102
|
+
tohrmid: '',
|
|
103
|
+
fromhrmid: '',
|
|
104
|
+
__random__: String(Date.now()),
|
|
105
|
+
};
|
|
106
|
+
// 文件夹选择器:虚拟文件夹(待办/标星…)用 selectorField + menu_<selectorField>(替代 menu_folderid)
|
|
107
|
+
if (f.selectorField) {
|
|
108
|
+
const v = f.selectorValue ?? '1';
|
|
109
|
+
q[f.selectorField] = v;
|
|
110
|
+
q[`menu_${f.selectorField}`] = v;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
q.menu_folderid = f.folderid;
|
|
114
|
+
}
|
|
115
|
+
// 收件箱类才带 receivemail/receivemailid(已发送抓包里没有这两个 key)
|
|
116
|
+
if (f.receivemail) {
|
|
117
|
+
q.receivemail = 'true';
|
|
118
|
+
q.receivemailid = f.receivemailid ?? '0';
|
|
119
|
+
}
|
|
120
|
+
if (opts.datetype)
|
|
121
|
+
q.datetype = opts.datetype;
|
|
122
|
+
if (opts.startdate)
|
|
123
|
+
q.startdate = opts.startdate;
|
|
124
|
+
if (opts.enddate)
|
|
125
|
+
q.enddate = opts.enddate;
|
|
126
|
+
return q;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 容错提取邮件行数组。实跑确认:行在 `tableBean.datas`(与待办 datas 同构,只是嵌套一层)。
|
|
130
|
+
* 候选路径保留多个,应对不同文件夹/接口变体。
|
|
131
|
+
*/
|
|
132
|
+
export function extractEmailItems(resp) {
|
|
133
|
+
const arr = pickByPaths(resp, [
|
|
134
|
+
'tableBean.datas',
|
|
135
|
+
'datas',
|
|
136
|
+
'data.datas',
|
|
137
|
+
'data.list',
|
|
138
|
+
'mailList',
|
|
139
|
+
'data.mailList',
|
|
140
|
+
'data.mails',
|
|
141
|
+
'rows',
|
|
142
|
+
'data.rows',
|
|
143
|
+
'list',
|
|
144
|
+
'result',
|
|
145
|
+
'result.list',
|
|
146
|
+
]);
|
|
147
|
+
if (Array.isArray(arr))
|
|
148
|
+
return arr.map(normalizeEmail);
|
|
149
|
+
if (arr && typeof arr === 'object' && !Array.isArray(arr)) {
|
|
150
|
+
const vals = Object.values(arr).filter((v) => v && typeof v === 'object');
|
|
151
|
+
if (vals.length)
|
|
152
|
+
return vals.map(normalizeEmail);
|
|
153
|
+
}
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
/** 从 allList 响应里提取总数。实跑确认在 `tableBean.total`。 */
|
|
157
|
+
export function extractEmailTotal(resp) {
|
|
158
|
+
const v = pickByPaths(resp, [
|
|
159
|
+
'tableBean.total',
|
|
160
|
+
'total',
|
|
161
|
+
'data.total',
|
|
162
|
+
'totalCount',
|
|
163
|
+
'data.totalCount',
|
|
164
|
+
'count',
|
|
165
|
+
'data.count',
|
|
166
|
+
'totalSize',
|
|
167
|
+
'data.totalSize',
|
|
168
|
+
]);
|
|
169
|
+
if (typeof v === 'number')
|
|
170
|
+
return v;
|
|
171
|
+
if (typeof v === 'string' && /^\d+$/.test(v))
|
|
172
|
+
return parseInt(v, 10);
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 把一行邮件数据映射成标准化邮件项。字段名实跑确认(来自 tableBean.columns 的 dataIndex
|
|
177
|
+
* 与行对象字段):
|
|
178
|
+
* id / subject / sendfromSpan(对象,realName/title) / senddate(含时分) /
|
|
179
|
+
* sizeSpan(人类可读) / size(字节) / flagSpan{labelId:"已读"|"未读"} /
|
|
180
|
+
* starred / hasAttachment / priority / folderid
|
|
181
|
+
* span 字段(sendfromSpan/toSpan)是对象不是 HTML 字符串,取其 realName/title。
|
|
182
|
+
*/
|
|
183
|
+
export function normalizeEmail(row) {
|
|
184
|
+
if (!row || typeof row !== 'object')
|
|
185
|
+
return {};
|
|
186
|
+
const get = (keys) => pickByPaths(row, keys);
|
|
187
|
+
// span 字段可能是 HTML 字串或对象(sendfromSpan={realName,title,...});对象取 realName/title
|
|
188
|
+
const spanField = (keys) => {
|
|
189
|
+
const v = get(keys);
|
|
190
|
+
if (v == null)
|
|
191
|
+
return undefined;
|
|
192
|
+
if (typeof v === 'string')
|
|
193
|
+
return stripHtml(v) || undefined;
|
|
194
|
+
if (typeof v === 'object')
|
|
195
|
+
return stringify(v.realName ?? v.title ?? v.label ?? v.name);
|
|
196
|
+
return undefined;
|
|
197
|
+
};
|
|
198
|
+
// 已读状态:flagSpan.labelId = "已读"/"未读"(中文)
|
|
199
|
+
const flagSpan = get(['flagSpan']);
|
|
200
|
+
let read;
|
|
201
|
+
if (flagSpan && typeof flagSpan === 'object') {
|
|
202
|
+
const label = stringify(flagSpan.labelId);
|
|
203
|
+
if (label === '已读')
|
|
204
|
+
read = true;
|
|
205
|
+
else if (label === '未读')
|
|
206
|
+
read = false;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
id: stringify(get(['id', 'mailid', 'mailId', 'messageId', 'content_uuid'])),
|
|
210
|
+
subject: stringify(get(['subject', 'mailsubject', 'title'])),
|
|
211
|
+
from: spanField(['sendfromSpan', 'fromspan', 'sender', 'fromName', 'sendperson']),
|
|
212
|
+
fromAddress: stringify(get(['fromAddress', 'fromaddress', 'senderAddress', 'email', 'senderemail', 'mailfrom'])),
|
|
213
|
+
to: spanField(['toSpan', 'tospan', 'to', 'toAddress', 'toaddress']),
|
|
214
|
+
date: stringify(get(['senddate', 'sentdate', 'receivedate', 'createdate', 'date', 'maildate'])),
|
|
215
|
+
size: stringify(get(['sizeSpan', 'size', 'mailsize', 'fileSize'])) || undefined,
|
|
216
|
+
read,
|
|
217
|
+
starred: toBool(get(['starred', 'isstar', 'isStar', 'star'])),
|
|
218
|
+
hasAttachment: toBool(get(['hasAttachment', 'attachmentNumber', 'attachcount'])),
|
|
219
|
+
importance: stringify(get(['priority', 'importance', 'classification', 'maillevel'])),
|
|
220
|
+
folderid: stringify(get(['folderid', 'folderId', 'mailfolderid'])),
|
|
221
|
+
raw: row,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/** 把任意"真假"值转 boolean(true/'true'/'1'/1 → true,其余 false)。 */
|
|
225
|
+
function toBool(v) {
|
|
226
|
+
if (v === true)
|
|
227
|
+
return true;
|
|
228
|
+
if (typeof v === 'number')
|
|
229
|
+
return v !== 0;
|
|
230
|
+
if (typeof v === 'string')
|
|
231
|
+
return /^(true|1|yes|y)$/i.test(v.trim());
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
function stringify(v) {
|
|
235
|
+
if (v === undefined || v === null || v === '')
|
|
236
|
+
return undefined;
|
|
237
|
+
return String(v);
|
|
238
|
+
}
|
|
239
|
+
/** 关键词 → 内部枚举名(中英文别名都映射到同一个)。 */
|
|
240
|
+
const RANGE_ALIASES = {
|
|
241
|
+
today: 'today', '今天': 'today',
|
|
242
|
+
yesterday: 'yesterday', '昨天': 'yesterday',
|
|
243
|
+
'day-before-yesterday': 'day-2', '前天': 'day-2',
|
|
244
|
+
'day-3': 'day-3', '大前天': 'day-3',
|
|
245
|
+
'this-week': 'this-week', '本周': 'this-week', '这周': 'this-week',
|
|
246
|
+
'last-week': 'last-week', '上周': 'last-week',
|
|
247
|
+
'this-month': 'this-month', '本月': 'this-month',
|
|
248
|
+
'last-month': 'last-month', '上月': 'last-month',
|
|
249
|
+
'recent-7': 'recent-7', '最近7天': 'recent-7', '近7天': 'recent-7',
|
|
250
|
+
'recent-30': 'recent-30', '最近30天': 'recent-30', '近30天': 'recent-30',
|
|
251
|
+
};
|
|
252
|
+
/**
|
|
253
|
+
* 把语义化关键词解析成具体起止日期(YYYY-MM-DD,本地时区)。
|
|
254
|
+
* 区间一律取「过去或当天」:end 不超今天(今天=本周/本月的当前日,上周/上月取完整自然段)。
|
|
255
|
+
* 通用模式「前N天 / N天前」(N≥1,如 前天=前2天、3天前)也认,返回当天往前 N 天的单天区间。
|
|
256
|
+
* 不认识的关键词返回 null(由调用方提示支持列表)。
|
|
257
|
+
*/
|
|
258
|
+
export function resolveDateRange(input) {
|
|
259
|
+
const norm = input.trim().toLowerCase();
|
|
260
|
+
// 通用:前N天 / N天前 → 当天往前 N 天(单天区间)。与固定关键词不冲突(\d+ 不匹配「前天」)。
|
|
261
|
+
const m = norm.match(/^前(\d+)天$/) ?? norm.match(/^(\d+)天前$/);
|
|
262
|
+
if (m) {
|
|
263
|
+
const n = parseInt(m[1], 10);
|
|
264
|
+
if (n >= 1)
|
|
265
|
+
return { startdate: offsetYmd(-n), enddate: offsetYmd(-n) };
|
|
266
|
+
}
|
|
267
|
+
const key = RANGE_ALIASES[norm];
|
|
268
|
+
if (!key)
|
|
269
|
+
return null;
|
|
270
|
+
const now = new Date();
|
|
271
|
+
const dow = (now.getDay() + 6) % 7; // 周一=0 … 周日=6
|
|
272
|
+
switch (key) {
|
|
273
|
+
case 'today':
|
|
274
|
+
return { startdate: todayStr(), enddate: todayStr() };
|
|
275
|
+
case 'yesterday':
|
|
276
|
+
return { startdate: offsetYmd(-1), enddate: offsetYmd(-1) };
|
|
277
|
+
case 'day-2':
|
|
278
|
+
return { startdate: offsetYmd(-2), enddate: offsetYmd(-2) }; // 前天
|
|
279
|
+
case 'day-3':
|
|
280
|
+
return { startdate: offsetYmd(-3), enddate: offsetYmd(-3) }; // 大前天
|
|
281
|
+
case 'this-week':
|
|
282
|
+
return { startdate: offsetYmd(-dow), enddate: todayStr() }; // 本周一 ~ 今天
|
|
283
|
+
case 'last-week':
|
|
284
|
+
return { startdate: offsetYmd(-dow - 7), enddate: offsetYmd(-dow - 1) }; // 上周一 ~ 上周日
|
|
285
|
+
case 'this-month':
|
|
286
|
+
return { startdate: toYmd(new Date(now.getFullYear(), now.getMonth(), 1)), enddate: todayStr() };
|
|
287
|
+
case 'last-month':
|
|
288
|
+
return {
|
|
289
|
+
startdate: toYmd(new Date(now.getFullYear(), now.getMonth() - 1, 1)),
|
|
290
|
+
enddate: toYmd(new Date(now.getFullYear(), now.getMonth(), 0)), // 上月最后一天(日=0=上月末)
|
|
291
|
+
};
|
|
292
|
+
case 'recent-7':
|
|
293
|
+
return { startdate: offsetYmd(-6), enddate: todayStr() }; // 含今天共 7 天
|
|
294
|
+
case 'recent-30':
|
|
295
|
+
return { startdate: offsetYmd(-29), enddate: todayStr() }; // 含今天共 30 天
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
/** 今天 YYYY-MM-DD(本地时区,避免 UTC 偏移)。 */
|
|
300
|
+
function todayStr() {
|
|
301
|
+
return toYmd(new Date());
|
|
302
|
+
}
|
|
303
|
+
/** 任意 Date → YYYY-MM-DD(本地时区,去掉 UTC 偏移后再取 ISO 日期)。 */
|
|
304
|
+
function toYmd(d) {
|
|
305
|
+
const tz = d.getTimezoneOffset() * 60000;
|
|
306
|
+
return new Date(d.getTime() - tz).toISOString().slice(0, 10);
|
|
307
|
+
}
|
|
308
|
+
/** 今天偏移 n 天的 YYYY-MM-DD(n 可负)。 */
|
|
309
|
+
function offsetYmd(n) {
|
|
310
|
+
const d = new Date();
|
|
311
|
+
d.setDate(d.getDate() + n);
|
|
312
|
+
return toYmd(d);
|
|
313
|
+
}
|
|
314
|
+
export async function getEmailView(cfg, mailId) {
|
|
315
|
+
const api = await createApiFromProfile(cfg);
|
|
316
|
+
const rmv = await api.get('/api/email/view/mailView', { mailId });
|
|
317
|
+
const rc = await api.get('/api/email/view/mailContentView', { mailId });
|
|
318
|
+
return {
|
|
319
|
+
meta: normalizeMailView(rmv.json, mailId),
|
|
320
|
+
contentText: extractMailContentText(rc.json),
|
|
321
|
+
contentHtml: extractMailContentHtml(rc.json),
|
|
322
|
+
rawMailView: rmv.json,
|
|
323
|
+
rawContent: rc.json,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* 标记邮件已读/未读。抓包:GET /api/email/base/setReadStatus?mailIds=<id>&status=1&type=
|
|
328
|
+
* status=1 已读 / 0 未读。是写操作(改状态),CLI 默认不调用,需 --mark-read 显式触发。
|
|
329
|
+
*/
|
|
330
|
+
export async function setMailReadStatus(cfg, mailId, read = true) {
|
|
331
|
+
const api = await createApiFromProfile(cfg);
|
|
332
|
+
const r = await api.get('/api/email/base/setReadStatus', { mailIds: mailId, status: read ? '1' : '0', type: '' });
|
|
333
|
+
const j = r.json;
|
|
334
|
+
return j?.status === '1' || j?.status === true || j?.success === true || r.ok;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* 从 mailView 响应提取邮件元信息。实跑确认结构:
|
|
338
|
+
* 顶层 viewBean(不是 data/bean),含 subject / senddate /
|
|
339
|
+
* fromSpan{showName,email} 单对象 /
|
|
340
|
+
* toSpan / ccSpan{list:[{showName,email},...]} 收件人组 /
|
|
341
|
+
* fileInfos[] 附件 / priority
|
|
342
|
+
*/
|
|
343
|
+
export function normalizeMailView(resp, mailId) {
|
|
344
|
+
if (!resp || typeof resp !== 'object')
|
|
345
|
+
return {};
|
|
346
|
+
const vb = pickByPaths(resp, ['viewBean', 'data.viewBean', 'bean', 'data', 'result']) ?? resp;
|
|
347
|
+
const src = vb && typeof vb === 'object' ? vb : resp;
|
|
348
|
+
const get = (keys) => pickByPaths(src, keys);
|
|
349
|
+
// 单个收发人对象(fromSpan):取 showName(实跑确认),回退 realName/title/email;去 <email> 后缀
|
|
350
|
+
const personField = (keys) => {
|
|
351
|
+
const v = get(keys);
|
|
352
|
+
if (v == null)
|
|
353
|
+
return undefined;
|
|
354
|
+
if (typeof v === 'string')
|
|
355
|
+
return stripHtml(v) || undefined;
|
|
356
|
+
if (typeof v === 'object' && !Array.isArray(v)) {
|
|
357
|
+
return stripHtml(stringify(v.showName ?? v.realName ?? v.title ?? v.name ?? v.label)) || stringify(v.email);
|
|
358
|
+
}
|
|
359
|
+
return undefined;
|
|
360
|
+
};
|
|
361
|
+
// 收件人组(toSpan/ccSpan):{list:[{showName,email},...]} → 展开拼 showName(空格分隔,去 <email>)
|
|
362
|
+
const recipientsField = (keys) => {
|
|
363
|
+
const v = get(keys);
|
|
364
|
+
if (v == null)
|
|
365
|
+
return undefined;
|
|
366
|
+
if (typeof v === 'string')
|
|
367
|
+
return stripHtml(v) || undefined;
|
|
368
|
+
if (Array.isArray(v))
|
|
369
|
+
return v.map((p) => stripHtml(stringify(p?.showName ?? p?.email))).filter(Boolean).join(' ') || undefined;
|
|
370
|
+
if (typeof v === 'object') {
|
|
371
|
+
if (Array.isArray(v.list))
|
|
372
|
+
return v.list.map((p) => stripHtml(stringify(p?.showName ?? p?.email))).filter(Boolean).join(' ') || undefined;
|
|
373
|
+
return stripHtml(stringify(v.showName ?? v.realName ?? v.email)) || undefined;
|
|
374
|
+
}
|
|
375
|
+
return undefined;
|
|
376
|
+
};
|
|
377
|
+
const fromObj = get(['fromSpan', 'sendfromSpan', 'from']);
|
|
378
|
+
const fileInfos = get(['fileInfos', 'attList', 'attachments', 'fileList', 'files', 'mailAttachmentList']);
|
|
379
|
+
// fileInfos 实抓包为对象 {count,totalSizeStr,fileList:[{filename,filesizeByte,fileid},...],batchDownloadUrl},
|
|
380
|
+
// 兼容数组/对象两种形态;取 fileList/list 作为附件数组。
|
|
381
|
+
let attachmentList = fileInfos;
|
|
382
|
+
if (fileInfos && typeof fileInfos === 'object' && !Array.isArray(fileInfos)) {
|
|
383
|
+
if (Array.isArray(fileInfos.fileList))
|
|
384
|
+
attachmentList = fileInfos.fileList;
|
|
385
|
+
else if (Array.isArray(fileInfos.list))
|
|
386
|
+
attachmentList = fileInfos.list;
|
|
387
|
+
else
|
|
388
|
+
attachmentList = [];
|
|
389
|
+
}
|
|
390
|
+
let attachments;
|
|
391
|
+
if (Array.isArray(attachmentList)) {
|
|
392
|
+
attachments = attachmentList.map((a) => ({
|
|
393
|
+
name: stringify(a?.filename ?? a?.name ?? a?.showName ?? a?.fileName),
|
|
394
|
+
size: stringify(a?.filesize ?? a?.size ?? a?.fileSize ?? a?.filesizeByte),
|
|
395
|
+
fileid: stringify(a?.fileid ?? a?.id ?? a?.imagefileid ?? a?.fileId),
|
|
396
|
+
}));
|
|
397
|
+
if (attachments.length === 0)
|
|
398
|
+
attachments = undefined;
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
id: stringify(get(['id', 'mailid', 'mailId'])) ?? mailId,
|
|
402
|
+
subject: stringify(get(['subjectText', 'subject', 'mailsubject', 'title'])),
|
|
403
|
+
from: personField(['fromSpan', 'sendfromSpan', 'sendfrom', 'from', 'sender']),
|
|
404
|
+
fromAddress: typeof fromObj === 'object' && fromObj ? stringify(fromObj.email ?? fromObj.senderAddress) : stringify(get(['fromAddress', 'email', 'senderemail', 'mailfrom'])),
|
|
405
|
+
to: recipientsField(['toSpan', 'sendtoSpan', 'sendto', 'to', 'receivers']),
|
|
406
|
+
cc: recipientsField(['ccSpan', 'sendccSpan', 'sendcc', 'cc', 'copyto']),
|
|
407
|
+
senddate: stringify(get(['senddate', 'sentdate', 'sendDate', 'sendtime'])),
|
|
408
|
+
receivedate: stringify(get(['receivedate', 'receiveDate', 'inboxdate'])),
|
|
409
|
+
size: stringify(get(['sizeSpan', 'size', 'mailsize', 'fileSize'])),
|
|
410
|
+
hasAttachment: attachments ? attachments.length > 0 : toBool(get(['hasAttachment', 'attachmentNumber'])),
|
|
411
|
+
attachments,
|
|
412
|
+
importance: stringify(get(['priority', 'importance', 'classification', 'maillevel'])),
|
|
413
|
+
raw: resp,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/** 从 mailContentView 响应提取正文 HTML。实跑确认:正文在 mailContent,是 base64 编码的 HTML(与微博 content 同)。 */
|
|
417
|
+
export function extractMailContentHtml(resp) {
|
|
418
|
+
const v = pickByPaths(resp, [
|
|
419
|
+
'mailContent',
|
|
420
|
+
'data.mailContent',
|
|
421
|
+
'content',
|
|
422
|
+
'data.content',
|
|
423
|
+
'html',
|
|
424
|
+
'data.html',
|
|
425
|
+
]);
|
|
426
|
+
if (typeof v === 'string' && v.trim()) {
|
|
427
|
+
return decodeBase64Html(v) ?? v; // base64 解码;解码不像 HTML 则按原文返回
|
|
428
|
+
}
|
|
429
|
+
if (v && typeof v === 'object') {
|
|
430
|
+
const s = stringify(v.content ?? v.html ?? v.mailContent);
|
|
431
|
+
if (s)
|
|
432
|
+
return decodeBase64Html(s) ?? s;
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
/** 正文纯文本(strip HTML + 压缩空白)。 */
|
|
437
|
+
export function extractMailContentText(resp) {
|
|
438
|
+
const html = extractMailContentHtml(resp);
|
|
439
|
+
if (!html)
|
|
440
|
+
return null;
|
|
441
|
+
const text = stripHtml(html);
|
|
442
|
+
return text ? text.replace(/\s+/g, ' ').trim() : null;
|
|
443
|
+
}
|
|
444
|
+
/** E9 邮件正文是 base64 编码的 HTML;解码后若像 HTML 则返回,否则返回 null(按原文处理)。 */
|
|
445
|
+
function decodeBase64Html(v) {
|
|
446
|
+
if (typeof v !== 'string' || !v)
|
|
447
|
+
return null;
|
|
448
|
+
const clean = v.replace(/\s+/g, '');
|
|
449
|
+
try {
|
|
450
|
+
const txt = Buffer.from(clean, 'base64').toString('utf8');
|
|
451
|
+
if (txt && /<[a-zA-Z!]/.test(txt))
|
|
452
|
+
return txt;
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
export async function getEmailComposeInit(cfg) {
|
|
460
|
+
const api = await createApiFromProfile(cfg);
|
|
461
|
+
const r = await api.get('/api/email/add/emailAdd', {
|
|
462
|
+
menuIds: '536,53601',
|
|
463
|
+
menuPathIds: '536,53601',
|
|
464
|
+
__random__: String(Date.now()),
|
|
465
|
+
});
|
|
466
|
+
const j = r.json;
|
|
467
|
+
const bean = j?.emailAddBean ?? {};
|
|
468
|
+
return {
|
|
469
|
+
sessionUUid: stringify(j?.email_sendsessionUUid) ?? randomUUID(),
|
|
470
|
+
isInternal: stringify(bean.isInternal),
|
|
471
|
+
flag: stringify(bean.flag),
|
|
472
|
+
mailId: stringify(bean.mailId),
|
|
473
|
+
raw: j,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* 取邮件账号配置:GET /api/email/base/getEmailAllConfig。
|
|
478
|
+
* 多路径容错提取 classification + hrmAccountid(响应结构未实跑确认,候选路径多个)。
|
|
479
|
+
* hrmAccountid 取不到时回退 getAccountList 的 userid;classification 回退默认 4。
|
|
480
|
+
*/
|
|
481
|
+
export async function getEmailAccountConfig(cfg) {
|
|
482
|
+
const api = await createApiFromProfile(cfg);
|
|
483
|
+
const r = await api.get('/api/email/base/getEmailAllConfig');
|
|
484
|
+
const j = r.json;
|
|
485
|
+
const accounts = pickByPaths(j, [
|
|
486
|
+
'apiEmails', 'data.apiEmails',
|
|
487
|
+
'mailAccountList', 'data.mailAccountList',
|
|
488
|
+
'accountlist', 'data.accountlist',
|
|
489
|
+
'accounts', 'data.accounts',
|
|
490
|
+
'emailAccounts', 'data.emailAccounts',
|
|
491
|
+
'data', 'list', 'data.list',
|
|
492
|
+
]);
|
|
493
|
+
let classification = '4';
|
|
494
|
+
let hrmAccountid = '';
|
|
495
|
+
const arr = Array.isArray(accounts)
|
|
496
|
+
? accounts
|
|
497
|
+
: accounts && typeof accounts === 'object' && !Array.isArray(accounts)
|
|
498
|
+
? Object.values(accounts).filter((v) => v && typeof v === 'object')
|
|
499
|
+
: [];
|
|
500
|
+
if (arr.length > 0) {
|
|
501
|
+
const cur = arr.find((a) => String(a.isdefault ?? a.isDefault ?? a.iscurrent ?? a.isCurrent) === '1') ??
|
|
502
|
+
arr[0];
|
|
503
|
+
classification = stringify(cur?.classification ?? cur?.classificationid ?? cur?.accountType ?? cur?.id) ?? '4';
|
|
504
|
+
hrmAccountid = stringify(cur?.hrmAccountid ?? cur?.hrmaccountid ?? cur?.accountId ?? cur?.userid ?? '') ?? '';
|
|
505
|
+
}
|
|
506
|
+
// hrmAccountid 取不到 → 回退当前用户 userid(抓包 hrmAccountid=1 即系统管理员 userid)
|
|
507
|
+
if (!hrmAccountid) {
|
|
508
|
+
try {
|
|
509
|
+
const u = await getCurrentUser(cfg);
|
|
510
|
+
hrmAccountid = u.userid;
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
hrmAccountid = '';
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return { classification, hrmAccountid, raw: j };
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* 把 id 列表字符串(逗号分隔,兼容中文逗号)解析为去重 id 数组。
|
|
520
|
+
* "56,78" / "56,78" → ["56","78"];空串 → []。
|
|
521
|
+
*/
|
|
522
|
+
function parseIdList(s) {
|
|
523
|
+
if (!s)
|
|
524
|
+
return [];
|
|
525
|
+
const seen = new Set();
|
|
526
|
+
const out = [];
|
|
527
|
+
for (const seg of s.split(/[,,]/)) {
|
|
528
|
+
const id = seg.trim();
|
|
529
|
+
if (id && !seen.has(id)) {
|
|
530
|
+
seen.add(id);
|
|
531
|
+
out.push(id);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return out;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* 构造送信请求体(复刻抓包 [32] 的全字段集)。
|
|
538
|
+
* 内部收件人 → internaltonew JSON 数组 + internaltonewIds;外部 → to/cc/bcc。
|
|
539
|
+
* isInternal 有任一内部收件人则为 1。mouldtext 按 contentIsHtml 决定原样 HTML 或纯文本包 <p>。
|
|
540
|
+
* sessionUUid 必须用 emailAdd 接口签发的值(服务端校验),不能客户端自造。
|
|
541
|
+
*/
|
|
542
|
+
export function buildSendBody(opts) {
|
|
543
|
+
const internalToIds = parseIdList(opts.internalTo);
|
|
544
|
+
const internalCcIds = parseIdList(opts.internalCc);
|
|
545
|
+
const internalBccIds = parseIdList(opts.internalBcc);
|
|
546
|
+
const hasInternal = internalToIds.length > 0 || internalCcIds.length > 0 || internalBccIds.length > 0;
|
|
547
|
+
const toInternalJson = (ids) => JSON.stringify(ids.map((id) => ({ id, type: 'resource', userids: id })));
|
|
548
|
+
const mouldtext = opts.contentIsHtml ? opts.content : textToMailHtml(opts.content);
|
|
549
|
+
// 草稿再发(mailid 给值)vs 新建(默认):前者 mailid=oldmailid=id + flag=4,后者 ""/-1/-1
|
|
550
|
+
const isDraft = !!opts.mailid;
|
|
551
|
+
return {
|
|
552
|
+
mailid: opts.mailid ?? '',
|
|
553
|
+
mailAccountId: '',
|
|
554
|
+
hrmAccountid: opts.hrmAccountid,
|
|
555
|
+
isInternal: hasInternal ? '1' : '0',
|
|
556
|
+
isSendApart: '0',
|
|
557
|
+
flag: isDraft ? '4' : '-1',
|
|
558
|
+
internaltonew: toInternalJson(internalToIds),
|
|
559
|
+
internalccnew: toInternalJson(internalCcIds),
|
|
560
|
+
internalbccnew: toInternalJson(internalBccIds),
|
|
561
|
+
internaltonewIds: internalToIds.join(','),
|
|
562
|
+
internalccnewIds: internalCcIds.join(','),
|
|
563
|
+
internalbccnewIds: internalBccIds.join(','),
|
|
564
|
+
to: opts.to ?? '',
|
|
565
|
+
cc: opts.cc ?? '',
|
|
566
|
+
bcc: opts.bcc ?? '',
|
|
567
|
+
usernameTo: '[]',
|
|
568
|
+
usernameCc: '[]',
|
|
569
|
+
usernameBcc: '[]',
|
|
570
|
+
subject: opts.subject,
|
|
571
|
+
mouldtext,
|
|
572
|
+
hasfile: opts.attachmentIds?.length ? '1' : '0',
|
|
573
|
+
oldmailid: opts.mailid ?? '-1',
|
|
574
|
+
accids: (opts.attachmentIds ?? []).join(','),
|
|
575
|
+
delaccids: '',
|
|
576
|
+
priority: opts.priority ?? '0',
|
|
577
|
+
savesend: '1',
|
|
578
|
+
texttype: '0',
|
|
579
|
+
needReceipt: opts.needReceipt ? '1' : '0',
|
|
580
|
+
op_isTimingSend: opts.timingDate ? '1' : '0',
|
|
581
|
+
timingdate: opts.timingDate ?? '',
|
|
582
|
+
sessionUUid: opts.sessionUUid,
|
|
583
|
+
classification: opts.classification,
|
|
584
|
+
secretDeadline: '',
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* 发送邮件。默认 dry-run(只构造请求体 + 解析账号配置,不发送);send=true 时才真发。
|
|
589
|
+
* hrmAccountid/classification 优先用 opts 显式值,否则从 getEmailAccountConfig 取。
|
|
590
|
+
* sessionUUid 必须用 emailAdd 接口签发的值(服务端校验),故 dry-run 与真发都先调 emailAdd。
|
|
591
|
+
*/
|
|
592
|
+
export async function sendEmail(cfg, opts) {
|
|
593
|
+
const ep = endpointFor(cfg);
|
|
594
|
+
// emailAdd:拿服务端签发的 sessionUUid(必用,否则 isSend:false2 不发送)
|
|
595
|
+
const composeInit = await getEmailComposeInit(cfg);
|
|
596
|
+
// 账号配置:opts 未显式给则查 getEmailAllConfig(GET,无副作用)
|
|
597
|
+
let classification = opts.classification;
|
|
598
|
+
let hrmAccountid = opts.hrmAccountid;
|
|
599
|
+
let accountConfig;
|
|
600
|
+
if (classification && hrmAccountid) {
|
|
601
|
+
accountConfig = { classification, hrmAccountid, raw: null };
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
accountConfig = await getEmailAccountConfig(cfg);
|
|
605
|
+
if (!classification)
|
|
606
|
+
classification = accountConfig.classification;
|
|
607
|
+
if (!hrmAccountid)
|
|
608
|
+
hrmAccountid = accountConfig.hrmAccountid;
|
|
609
|
+
}
|
|
610
|
+
// 附件:attachmentFiles=[本地路径,...] → 逐个 uploadFile 拿 fileid → accids。
|
|
611
|
+
// dry-run 不上传(避免留孤儿文件),仅展示待上传路径;send=true 才真传。
|
|
612
|
+
let attachmentIds = [];
|
|
613
|
+
let uploadedAttachments = [];
|
|
614
|
+
if (opts.attachmentFiles?.length) {
|
|
615
|
+
if (opts.send) {
|
|
616
|
+
for (const f of opts.attachmentFiles) {
|
|
617
|
+
const up = await uploadEmailAttachment(cfg, f);
|
|
618
|
+
if (!up.ok || !up.data?.id)
|
|
619
|
+
throw new Error(`附件上传失败:${f} → ${JSON.stringify(up.raw ?? {}).slice(0, 200)}`);
|
|
620
|
+
attachmentIds.push(String(up.data.id));
|
|
621
|
+
uploadedAttachments.push({ path: f, fileid: String(up.data.id), name: up.data?.filename ?? path.basename(f) });
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
uploadedAttachments = opts.attachmentFiles.map((f) => ({ path: f, fileid: null, name: path.basename(f) }));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const body = buildSendBody({
|
|
629
|
+
...opts,
|
|
630
|
+
attachmentIds,
|
|
631
|
+
hrmAccountid: hrmAccountid || '1',
|
|
632
|
+
classification: classification || '4',
|
|
633
|
+
sessionUUid: composeInit.sessionUUid,
|
|
634
|
+
});
|
|
635
|
+
if (!opts.send) {
|
|
636
|
+
return { sent: false, endpoint: { baseUrl: ep.baseUrl }, body, accountConfig, composeInit, uploadedAttachments };
|
|
637
|
+
}
|
|
638
|
+
const api = await createApiFromProfile(cfg);
|
|
639
|
+
const r = await api.post('/api/email/base/send', body);
|
|
640
|
+
return { sent: true, endpoint: { baseUrl: ep.baseUrl }, body, accountConfig, composeInit, uploadedAttachments, response: r };
|
|
641
|
+
}
|
|
642
|
+
/** 统一判成功:响应各字段(api_status/status/success/flag)任一真即成功。 */
|
|
643
|
+
function isOk(j, ok) {
|
|
644
|
+
return j?.api_status === true || j?.status === '1' || j?.status === true || j?.success === true || j?.flag === true || ok === true;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* 标星/取消标星。抓包:GET /api/email/base/setStar?mailIds=<id>&star=1|0&__random__=...
|
|
648
|
+
* star=true 标星 / false 取消标星。响应 {api_status:true, status:"1"}。
|
|
649
|
+
*/
|
|
650
|
+
export async function setEmailStar(cfg, mailIds, star = true) {
|
|
651
|
+
const api = await createApiFromProfile(cfg);
|
|
652
|
+
const r = await api.get('/api/email/base/setStar', { mailIds: parseIdList(mailIds).join(','), star: star ? '1' : '0', __random__: String(Date.now()) });
|
|
653
|
+
return { ok: isOk(r.json, r.ok), mailIds: parseIdList(mailIds), starred: !!star, raw: r.json };
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* 删除邮件。抓包:GET /api/email/base/deleteEmail?mailIds=<id>&operate=delete|deletePermanently
|
|
657
|
+
* operate=delete 移回收站(已删除)/ deletePermanently 彻底删除。
|
|
658
|
+
*/
|
|
659
|
+
export async function deleteEmails(cfg, mailIds, { permanent = false } = {}) {
|
|
660
|
+
const api = await createApiFromProfile(cfg);
|
|
661
|
+
const ids = parseIdList(mailIds);
|
|
662
|
+
const r = await api.get('/api/email/base/deleteEmail', { mailIds: ids.join(','), operate: permanent ? 'deletePermanently' : 'delete', __random__: String(Date.now()) });
|
|
663
|
+
return { ok: isOk(r.json, r.ok), mailIds: ids, permanent, raw: r.json };
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* 移动邮件到文件夹。抓包:POST /api/email/setting/operateUserMailFolder
|
|
667
|
+
* body: mailIds=<id>&folderId=<folderId>&operation=move
|
|
668
|
+
* 文件夹:0=收件箱 -1=已发送 -2=草稿箱 -3=已删除,其余为正数自定义文件夹 id。
|
|
669
|
+
*/
|
|
670
|
+
export async function moveEmails(cfg, mailIds, folderId) {
|
|
671
|
+
const api = await createApiFromProfile(cfg);
|
|
672
|
+
const ids = parseIdList(mailIds);
|
|
673
|
+
const r = await api.post('/api/email/setting/operateUserMailFolder', { mailIds: ids.join(','), folderId: String(folderId), operation: 'move' });
|
|
674
|
+
return { ok: isOk(r.json, r.ok), mailIds: ids, folderId: String(folderId), raw: r.json };
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* 标签列表。链路:GET /api/email/setting/getMailLabel → sessionkey → POST /api/ec/dev/table/datas
|
|
678
|
+
* (dataKey=sessionkey,同待办 table 链路)。行含 color/name/noreadcount/allcount。
|
|
679
|
+
*/
|
|
680
|
+
export async function getEmailLabels(cfg) {
|
|
681
|
+
const api = await createApiFromProfile(cfg);
|
|
682
|
+
const r1 = await api.get('/api/email/setting/getMailLabel', { __random__: String(Date.now()) });
|
|
683
|
+
const sessionkey = r1.json?.sessionkey;
|
|
684
|
+
if (!sessionkey)
|
|
685
|
+
return { sessionkey: null, items: [], raw: r1.json };
|
|
686
|
+
const r2 = await api.post('/api/ec/dev/table/datas', { dataKey: sessionkey, current: '1', sortParams: '[]' });
|
|
687
|
+
const rows = extractRows(r2.json);
|
|
688
|
+
const items = rows
|
|
689
|
+
.map((row) => ({
|
|
690
|
+
id: stringify(pickByPaths(row, ['id', 'randomFieldId'])),
|
|
691
|
+
name: stripHtml(pickByPaths(row, ['namespan', 'name'])) || stringify(pickByPaths(row, ['name'])),
|
|
692
|
+
color: stringify(pickByPaths(row, ['color'])),
|
|
693
|
+
unreadCount: stringify(pickByPaths(row, ['noreadcountspan', 'noreadcount'])),
|
|
694
|
+
totalCount: stringify(pickByPaths(row, ['allcountspan', 'allcount'])),
|
|
695
|
+
raw: row,
|
|
696
|
+
}))
|
|
697
|
+
.filter((it) => it.id || it.name);
|
|
698
|
+
return { sessionkey, items, raw: { getMailLabel: r1.json, datas: r2.json } };
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* 新建标签。抓包:POST /api/email/setting/operateUserMailLabel
|
|
702
|
+
* body: labelName=<名>&labelColor=<#hex>&operation=add → {flag:true, id:"<标签id>", status:"1"}
|
|
703
|
+
*/
|
|
704
|
+
export async function addEmailLabel(cfg, { name, color = '#000000' }) {
|
|
705
|
+
const api = await createApiFromProfile(cfg);
|
|
706
|
+
const r = await api.post('/api/email/setting/operateUserMailLabel', { labelName: name, labelColor: color, operation: 'add' });
|
|
707
|
+
const j = r.json;
|
|
708
|
+
return { ok: isOk(j, r.ok), id: stringify(j?.id), name, color, raw: j };
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* 给邮件贴/去标签。抓包:POST /api/email/setting/operateUserMailLabel
|
|
712
|
+
* body: mailIds=<id>&id=<labelId>&operation=addLabel(贴)/ removeLabel(去,名称未抓包、按惯例)
|
|
713
|
+
*/
|
|
714
|
+
export async function applyEmailLabel(cfg, mailIds, labelId, { remove = false } = {}) {
|
|
715
|
+
const api = await createApiFromProfile(cfg);
|
|
716
|
+
const ids = parseIdList(mailIds);
|
|
717
|
+
const r = await api.post('/api/email/setting/operateUserMailLabel', { mailIds: ids.join(','), id: String(labelId), operation: remove ? 'removeLabel' : 'addLabel' });
|
|
718
|
+
return { ok: isOk(r.json, r.ok), mailIds: ids, labelId: String(labelId), remove, raw: r.json };
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* 设置待办邮件。抓包:POST /api/email/waitdeal/addOrUpdateWaitDeal
|
|
722
|
+
* body: waitdealtime=<YYYY-MM-DD>&waitdealway=<5|1>&wdremindtime=<HH:mm>&waitdealnote=<备注>
|
|
723
|
+
* &op_hasNote=<0|1>&op_hasRemind=<0|1>&mailIds=<id>
|
|
724
|
+
* waitdealway:5=消息中心提醒(默认,前端选中) / 1=短信提醒(emailWaitdealCondition 实证)。
|
|
725
|
+
*/
|
|
726
|
+
export async function setEmailWaitdeal(cfg, mailIds, { time, way = '5', remindTime, note, hasNote = false, hasRemind = false } = {}) {
|
|
727
|
+
const api = await createApiFromProfile(cfg);
|
|
728
|
+
const ids = parseIdList(mailIds);
|
|
729
|
+
const body = {
|
|
730
|
+
waitdealtime: time ?? '',
|
|
731
|
+
waitdealway: String(way ?? '5'),
|
|
732
|
+
wdremindtime: remindTime ?? '',
|
|
733
|
+
waitdealnote: note ?? '',
|
|
734
|
+
op_hasNote: hasNote ? '1' : '0',
|
|
735
|
+
op_hasRemind: hasRemind ? '1' : '0',
|
|
736
|
+
mailIds: ids.join(','),
|
|
737
|
+
};
|
|
738
|
+
const r = await api.post('/api/email/waitdeal/addOrUpdateWaitDeal', body);
|
|
739
|
+
return { ok: isOk(r.json, r.ok), mailIds: ids, body, raw: r.json };
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* 撤回已发邮件。抓包:POST /api/email/view/recall(body: mailId=<id>)→ {flag:true, status:"1"}。
|
|
743
|
+
* 仅对未读/未删除的已发送内部邮件有效(E9 语义以服务端返回为准)。
|
|
744
|
+
*/
|
|
745
|
+
export async function recallEmail(cfg, mailId) {
|
|
746
|
+
const api = await createApiFromProfile(cfg);
|
|
747
|
+
const r = await api.post('/api/email/view/recall', { mailId: String(mailId) });
|
|
748
|
+
return { ok: isOk(r.json, r.ok), mailId: String(mailId), raw: r.json };
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* 上传邮件附件。抓包:POST /api/email/add/uploadFile(multipart: name/secretLevel/file)
|
|
752
|
+
* → {data:{id:"<fileid>", filename, filelink, filesize...}, status:"1"}。
|
|
753
|
+
* 返回的 fileid 用于 send 的 accids。走 cookie 直连(同 cube 附件上传模式)。
|
|
754
|
+
*/
|
|
755
|
+
export async function uploadEmailAttachment(cfg, filePath, { name } = {}) {
|
|
756
|
+
const ep = endpointFor(cfg);
|
|
757
|
+
const buf = readFileSync(filePath);
|
|
758
|
+
const filename = name ?? path.basename(filePath);
|
|
759
|
+
const fd = new FormData();
|
|
760
|
+
fd.append('name', filename);
|
|
761
|
+
fd.append('secretLevel', '');
|
|
762
|
+
fd.append('file', new Blob([buf]), filename);
|
|
763
|
+
const base = ep.baseUrl.replace(/\/+$/, '');
|
|
764
|
+
const r = await fetch(`${base}/api/email/add/uploadFile`, {
|
|
765
|
+
method: 'POST',
|
|
766
|
+
headers: { Cookie: ep.cookie, 'X-Requested-With': 'XMLHttpRequest', Referer: `${base}/spa/email/static/index.html` },
|
|
767
|
+
body: fd,
|
|
768
|
+
});
|
|
769
|
+
let j = null;
|
|
770
|
+
try {
|
|
771
|
+
j = await r.json();
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
j = null;
|
|
775
|
+
}
|
|
776
|
+
return { ok: j?.status === '1' || r.ok, data: j?.data ?? null, raw: j };
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* 附件列表。链路:GET /api/email/attachment/list?filename=&subject= → sessionkey
|
|
780
|
+
* → POST /api/ec/dev/table/datas(dataKey=sessionkey)。行含 filename/subject/mailid/filesize。
|
|
781
|
+
*/
|
|
782
|
+
export async function getEmailAttachmentList(cfg, { filename, subject } = {}) {
|
|
783
|
+
const api = await createApiFromProfile(cfg);
|
|
784
|
+
const r1 = await api.get('/api/email/attachment/list', { filename: filename ?? '', subject: subject ?? '', __random__: String(Date.now()) });
|
|
785
|
+
const sessionkey = r1.json?.sessionkey;
|
|
786
|
+
if (!sessionkey)
|
|
787
|
+
return { sessionkey: null, items: [], raw: r1.json };
|
|
788
|
+
const r2 = await api.post('/api/ec/dev/table/datas', { dataKey: sessionkey, current: '1', sortParams: '[]' });
|
|
789
|
+
const rows = extractRows(r2.json);
|
|
790
|
+
const items = rows
|
|
791
|
+
.map((row) => ({
|
|
792
|
+
id: stringify(pickByPaths(row, ['id'])),
|
|
793
|
+
mailid: stringify(pickByPaths(row, ['mailid', 'mailId'])),
|
|
794
|
+
filename: stripHtml(pickByPaths(row, ['filenamespan', 'filename'])) || stringify(pickByPaths(row, ['filename'])),
|
|
795
|
+
subject: stripHtml(pickByPaths(row, ['subjectspan', 'subject'])) || stringify(pickByPaths(row, ['subject'])),
|
|
796
|
+
size: stripHtml(pickByPaths(row, ['filesizespan', 'filesize'])) || stringify(pickByPaths(row, ['filesize'])),
|
|
797
|
+
raw: row,
|
|
798
|
+
}))
|
|
799
|
+
.filter((it) => it.id || it.filename);
|
|
800
|
+
return { sessionkey, items, raw: { list: r1.json, datas: r2.json } };
|
|
801
|
+
}
|
|
802
|
+
/** 邮箱容量/未读统计。抓包:GET /api/email/folder/getEmailSize → {unreadsize,totalSize,remainspace,occupyspace,totalspace}。 */
|
|
803
|
+
export async function getEmailSize(cfg) {
|
|
804
|
+
const api = await createApiFromProfile(cfg);
|
|
805
|
+
const r = await api.get('/api/email/folder/getEmailSize', { __random__: String(Date.now()) });
|
|
806
|
+
const j = r.json ?? {};
|
|
807
|
+
return {
|
|
808
|
+
unreadsize: stringify(j.unreadsize),
|
|
809
|
+
totalSize: stringify(j.totalSize),
|
|
810
|
+
remainspace: typeof j.remainspace === 'number' ? j.remainspace : stringify(j.remainspace),
|
|
811
|
+
occupyspace: typeof j.occupyspace === 'number' ? j.occupyspace : stringify(j.occupyspace),
|
|
812
|
+
totalspace: typeof j.totalspace === 'number' ? j.totalspace : stringify(j.totalspace),
|
|
813
|
+
spaceRight: stringify(j.spaceRight),
|
|
814
|
+
raw: j,
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
/** 接收账户列表。抓包:GET /api/email/base/getReciveAccount?receivemailid=0 → {accountList:[...]}。 */
|
|
818
|
+
export async function getEmailReciveAccounts(cfg) {
|
|
819
|
+
const api = await createApiFromProfile(cfg);
|
|
820
|
+
const r = await api.get('/api/email/base/getReciveAccount', { receivemailid: '0', __random__: String(Date.now()) });
|
|
821
|
+
const j = r.json ?? {};
|
|
822
|
+
return { items: Array.isArray(j.accountList) ? j.accountList : [], raw: j };
|
|
823
|
+
}
|
|
824
|
+
/** 纯文本转邮件正文 HTML:每行包 <p>,转义 HTML 特殊字符(与微博 textToBlogHtml 同构)。 */
|
|
825
|
+
export function textToMailHtml(text) {
|
|
826
|
+
return text
|
|
827
|
+
.split(/\r?\n/)
|
|
828
|
+
.map((l) => `<p>${escapeHtml(l)}</p>`)
|
|
829
|
+
.join('');
|
|
830
|
+
}
|
|
831
|
+
function escapeHtml(s) {
|
|
832
|
+
return s
|
|
833
|
+
.replace(/&/g, '&')
|
|
834
|
+
.replace(/</g, '<')
|
|
835
|
+
.replace(/>/g, '>')
|
|
836
|
+
.replace(/"/g, '"')
|
|
837
|
+
.replace(/'/g, ''');
|
|
838
|
+
}
|