dsh-email 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -10
- package/lib/config.d.ts +28 -8
- package/lib/config.js +80 -22
- package/lib/index.d.ts +3 -3
- package/lib/index.js +157 -68
- package/lib/mail-client.d.ts +36 -13
- package/lib/mail-client.js +286 -72
- package/lib/parse.d.ts +5 -0
- package/lib/parse.js +25 -6
- package/lib/types.d.ts +51 -9
- package/package.json +62 -53
package/lib/index.js
CHANGED
|
@@ -1,43 +1,41 @@
|
|
|
1
|
-
import { clampInt,
|
|
2
|
-
import {
|
|
1
|
+
import { clampInt, resolveEmailSettings } from './config.js';
|
|
2
|
+
import { EmailPool, messageOf } from './mail-client.js';
|
|
3
3
|
export const name = 'tool-email';
|
|
4
4
|
export const inject = ['tools'];
|
|
5
5
|
const MAX_LIMIT = 100;
|
|
6
|
-
|
|
6
|
+
const ACCOUNT_HINT = '账号名(配置了 accounts 多个账号时选择),省略时用 defaultAccount。可用账号见 email_folders 的报错或插件 README';
|
|
7
|
+
const strArray = { type: 'array', items: { type: 'string' } };
|
|
8
|
+
const addrArray = { type: 'array', items: { type: 'object', additionalProperties: true } };
|
|
9
|
+
const messageShape = {
|
|
10
|
+
uid: { type: 'integer' },
|
|
11
|
+
date: { type: 'string' },
|
|
12
|
+
from: addrArray,
|
|
13
|
+
subject: { type: 'string' },
|
|
14
|
+
seen: { type: 'boolean' },
|
|
15
|
+
flagged: { type: 'boolean' },
|
|
16
|
+
size: { type: 'integer' },
|
|
17
|
+
hasAttachments: { type: 'boolean' },
|
|
18
|
+
};
|
|
7
19
|
const listSchema = {
|
|
8
20
|
type: 'object',
|
|
9
21
|
properties: {
|
|
22
|
+
account: { type: 'string' },
|
|
10
23
|
count: { type: 'integer' },
|
|
11
24
|
folder: { type: 'string' },
|
|
12
|
-
messages: {
|
|
13
|
-
type: 'array',
|
|
14
|
-
items: {
|
|
15
|
-
type: 'object',
|
|
16
|
-
properties: {
|
|
17
|
-
uid: { type: 'integer' },
|
|
18
|
-
date: { type: 'string' },
|
|
19
|
-
from: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
20
|
-
subject: { type: 'string' },
|
|
21
|
-
seen: { type: 'boolean' },
|
|
22
|
-
flagged: { type: 'boolean' },
|
|
23
|
-
size: { type: 'integer' },
|
|
24
|
-
hasAttachments: { type: 'boolean' },
|
|
25
|
-
},
|
|
26
|
-
additionalProperties: true,
|
|
27
|
-
},
|
|
28
|
-
},
|
|
25
|
+
messages: { type: 'array', items: { type: 'object', properties: messageShape, additionalProperties: true } },
|
|
29
26
|
},
|
|
30
27
|
additionalProperties: true,
|
|
31
28
|
};
|
|
32
29
|
const readSchema = {
|
|
33
30
|
type: 'object',
|
|
34
31
|
properties: {
|
|
32
|
+
account: { type: 'string' },
|
|
35
33
|
uid: { type: 'integer' },
|
|
36
34
|
folder: { type: 'string' },
|
|
37
35
|
date: { type: 'string' },
|
|
38
|
-
from:
|
|
39
|
-
to:
|
|
40
|
-
cc:
|
|
36
|
+
from: addrArray,
|
|
37
|
+
to: addrArray,
|
|
38
|
+
cc: addrArray,
|
|
41
39
|
subject: { type: 'string' },
|
|
42
40
|
text: { type: 'string' },
|
|
43
41
|
attachments: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
@@ -48,13 +46,46 @@ const readSchema = {
|
|
|
48
46
|
const sendSchema = {
|
|
49
47
|
type: 'object',
|
|
50
48
|
properties: {
|
|
49
|
+
account: { type: 'string' },
|
|
51
50
|
messageId: { type: 'string' },
|
|
52
|
-
accepted:
|
|
53
|
-
rejected:
|
|
51
|
+
accepted: strArray,
|
|
52
|
+
rejected: strArray,
|
|
54
53
|
response: { type: 'string' },
|
|
55
54
|
},
|
|
56
55
|
additionalProperties: true,
|
|
57
56
|
};
|
|
57
|
+
const foldersSchema = {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
account: { type: 'string' },
|
|
61
|
+
folders: {
|
|
62
|
+
type: 'array',
|
|
63
|
+
items: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
name: { type: 'string' },
|
|
67
|
+
path: { type: 'string' },
|
|
68
|
+
specialUse: { type: 'string' },
|
|
69
|
+
subscribed: { type: 'boolean' },
|
|
70
|
+
},
|
|
71
|
+
additionalProperties: true,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
additionalProperties: true,
|
|
76
|
+
};
|
|
77
|
+
const attachmentSchema = {
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: {
|
|
80
|
+
account: { type: 'string' },
|
|
81
|
+
uid: { type: 'integer' },
|
|
82
|
+
filename: { type: 'string' },
|
|
83
|
+
contentType: { type: 'string' },
|
|
84
|
+
size: { type: 'integer' },
|
|
85
|
+
path: { type: 'string' },
|
|
86
|
+
},
|
|
87
|
+
additionalProperties: true,
|
|
88
|
+
};
|
|
58
89
|
function oneText(text) {
|
|
59
90
|
return [{ type: 'text', text }];
|
|
60
91
|
}
|
|
@@ -65,53 +96,78 @@ function describeMessage(message) {
|
|
|
65
96
|
message.flagged ? '已标星' : '',
|
|
66
97
|
message.hasAttachments ? '含附件' : '',
|
|
67
98
|
].filter(Boolean);
|
|
68
|
-
const parts = [
|
|
99
|
+
const parts = ['uid=' + message.uid, from, message.date];
|
|
69
100
|
if (flags.length > 0)
|
|
70
101
|
parts.push(flags.join('、'));
|
|
71
|
-
return
|
|
102
|
+
return (message.subject || '(无主题)') + ' [' + parts.join(' | ') + ']';
|
|
72
103
|
}
|
|
73
104
|
function renderList(value) {
|
|
74
105
|
if (value.messages.length === 0) {
|
|
75
|
-
return oneText(
|
|
106
|
+
return oneText('账号 ' + value.account + ',文件夹 "' + value.folder + '" 共 ' + value.count + ' 封邮件,本次没有要列出的邮件。');
|
|
76
107
|
}
|
|
77
|
-
const lines = value.messages.map((m, i) =>
|
|
78
|
-
return oneText(
|
|
108
|
+
const lines = value.messages.map((m, i) => '#' + (i + 1) + ' ' + describeMessage(m));
|
|
109
|
+
return oneText('账号 ' + value.account + ',文件夹 "' + value.folder + '" 共 ' + value.count + ' 封邮件,最新 ' + value.messages.length + ' 封:\n\n' + lines.join('\n') + '\n\n用 email_read 配合 uid 阅读全文。');
|
|
79
110
|
}
|
|
80
111
|
function renderRead(value) {
|
|
81
112
|
const from = value.from.map(a => a.name ?? a.address).filter(Boolean).join(', ') || '(未知)';
|
|
82
113
|
const attach = value.attachments.length > 0
|
|
83
|
-
?
|
|
114
|
+
? '\n附件:' + value.attachments.map((a, i) => '#' + i + ' ' + a.filename + '(' + a.contentType + ',' + a.size + ' 字节)').join(';') + '\n(用 email_attachment 配合 uid 与序号下载)'
|
|
84
115
|
: '';
|
|
85
|
-
return oneText(
|
|
116
|
+
return oneText('账号 ' + value.account + ',主题:' + (value.subject || '(无主题)') + '\n来自:' + from + '\n时间:' + (value.date || '(未知)') + attach + '\n\n' + value.text);
|
|
86
117
|
}
|
|
87
118
|
function renderSearch(value) {
|
|
88
119
|
if (value.messages.length === 0) {
|
|
89
|
-
return oneText(
|
|
120
|
+
return oneText('账号 ' + value.account + ',在文件夹 "' + value.folder + '" 中搜索 "' + value.query + '":共 ' + value.count + ' 条匹配,本次没有列出。');
|
|
90
121
|
}
|
|
91
|
-
const lines = value.messages.map((m, i) =>
|
|
92
|
-
return oneText(
|
|
122
|
+
const lines = value.messages.map((m, i) => '#' + (i + 1) + ' ' + describeMessage(m));
|
|
123
|
+
return oneText('账号 ' + value.account + ',在文件夹 "' + value.folder + '" 中搜索 "' + value.query + '":共 ' + value.count + ' 条匹配,展示最新 ' + value.messages.length + ' 条:\n\n' + lines.join('\n'));
|
|
93
124
|
}
|
|
94
125
|
function renderSend(value) {
|
|
95
|
-
const rejected = value.rejected.length > 0 ?
|
|
96
|
-
return oneText(
|
|
126
|
+
const rejected = value.rejected.length > 0 ? ';被拒:' + value.rejected.join(', ') : '';
|
|
127
|
+
return oneText('账号 ' + value.account + ' 邮件已发送,messageId: ' + value.messageId + ';成功送达:' + value.accepted.join(', ') + rejected + ';服务器响应:' + value.response);
|
|
128
|
+
}
|
|
129
|
+
function renderFolders(value) {
|
|
130
|
+
if (value.folders.length === 0)
|
|
131
|
+
return oneText('账号 ' + value.account + ':未列出任何文件夹。');
|
|
132
|
+
const lines = value.folders.map((f, i) => '#' + (i + 1) + ' ' + f.path + (f.specialUse !== '' ? ' [' + f.specialUse + ']' : '') + (f.subscribed ? '' : '(未订阅)'));
|
|
133
|
+
return oneText('账号 ' + value.account + ' 的文件夹(' + value.folders.length + ' 个):\n\n' + lines.join('\n') + '\n\n把 folder 参数填成其中的 path 即可。');
|
|
134
|
+
}
|
|
135
|
+
function renderAttachment(value) {
|
|
136
|
+
return oneText('账号 ' + value.account + ' 已下载附件 "' + value.filename + '"(' + value.contentType + ',' + value.size + ' 字节)到:\n' + value.path + '\n可用 read 工具读取该文件。');
|
|
97
137
|
}
|
|
98
138
|
export function apply(ctx, config = {}) {
|
|
99
|
-
|
|
139
|
+
let settingsError = '';
|
|
140
|
+
let settings = null;
|
|
100
141
|
try {
|
|
101
|
-
|
|
142
|
+
settings = resolveEmailSettings(config);
|
|
102
143
|
}
|
|
103
144
|
catch (error) {
|
|
104
|
-
|
|
145
|
+
settingsError = messageOf(error, 'dsh-email 配置无效');
|
|
146
|
+
ctx.logger?.warn?.('[dsh-email] ' + settingsError);
|
|
105
147
|
}
|
|
106
|
-
|
|
148
|
+
let pool = null;
|
|
149
|
+
const getPool = () => {
|
|
150
|
+
if (settingsError !== '')
|
|
151
|
+
throw new Error(settingsError);
|
|
152
|
+
if (pool === null) {
|
|
153
|
+
pool = new EmailPool(settings);
|
|
154
|
+
pool.startIdleSweep();
|
|
155
|
+
}
|
|
156
|
+
return pool;
|
|
157
|
+
};
|
|
158
|
+
ctx.effect(() => () => {
|
|
159
|
+
pool?.dispose();
|
|
160
|
+
pool = null;
|
|
161
|
+
});
|
|
107
162
|
ctx.tools.register({
|
|
108
163
|
name: 'email_list',
|
|
109
164
|
description: 'List recent emails in a mailbox folder (newest first). Returns uid, date, sender, subject and flags without message bodies; use email_read with a uid to fetch the full text.',
|
|
110
165
|
parameters: {
|
|
111
|
-
folder: { type: 'string', description:
|
|
166
|
+
folder: { type: 'string', description: 'IMAP folder path (see email_folders); defaults to the account inboxFolder' },
|
|
112
167
|
limit: { type: 'integer', description: 'How many messages to return, 1-100, default 20' },
|
|
113
168
|
offset: { type: 'integer', description: 'Skip this many newest messages first, default 0' },
|
|
114
169
|
unreadOnly: { type: 'boolean', description: 'Only list unread messages, default false' },
|
|
170
|
+
account: { type: 'string', description: ACCOUNT_HINT },
|
|
115
171
|
},
|
|
116
172
|
output: {
|
|
117
173
|
schema: listSchema,
|
|
@@ -119,18 +175,18 @@ export function apply(ctx, config = {}) {
|
|
|
119
175
|
},
|
|
120
176
|
async execute(rawArgs) {
|
|
121
177
|
const args = rawArgs;
|
|
122
|
-
const cfg = resolve();
|
|
123
178
|
const limit = clampInt(args.limit, 20, 1, MAX_LIMIT);
|
|
124
179
|
const offset = clampInt(args.offset, 0, 0, 10000);
|
|
125
|
-
return await
|
|
180
|
+
return await getPool().list(args.account, args.folder?.trim() || '', limit, offset, args.unreadOnly === true);
|
|
126
181
|
},
|
|
127
182
|
});
|
|
128
183
|
ctx.tools.register({
|
|
129
184
|
name: 'email_read',
|
|
130
|
-
description: 'Read one full email message by its uid (from email_list or email_search). Returns the plain-text body (HTML mail is converted; oversized bodies are truncated) plus attachment metadata.',
|
|
185
|
+
description: 'Read one full email message by its uid (from email_list or email_search). Returns the plain-text body (HTML mail is converted; oversized bodies are truncated) plus attachment metadata; use email_attachment to download one.',
|
|
131
186
|
parameters: {
|
|
132
187
|
uid: { type: 'integer', required: true, description: 'Message uid from email_list or email_search' },
|
|
133
|
-
folder: { type: 'string', description: 'IMAP folder the uid belongs to; defaults to the
|
|
188
|
+
folder: { type: 'string', description: 'IMAP folder the uid belongs to; defaults to the account inboxFolder' },
|
|
189
|
+
account: { type: 'string', description: ACCOUNT_HINT },
|
|
134
190
|
},
|
|
135
191
|
output: {
|
|
136
192
|
schema: readSchema,
|
|
@@ -138,11 +194,10 @@ export function apply(ctx, config = {}) {
|
|
|
138
194
|
},
|
|
139
195
|
async execute(rawArgs) {
|
|
140
196
|
const args = rawArgs;
|
|
141
|
-
const cfg = resolve();
|
|
142
197
|
if (typeof args.uid !== 'number' || !Number.isInteger(args.uid) || args.uid <= 0) {
|
|
143
198
|
throw new Error('uid 必须是正整数(用 email_list 获取)');
|
|
144
199
|
}
|
|
145
|
-
return await
|
|
200
|
+
return await getPool().read(args.account, args.uid, args.folder?.trim() || '');
|
|
146
201
|
},
|
|
147
202
|
});
|
|
148
203
|
ctx.tools.register({
|
|
@@ -150,8 +205,9 @@ export function apply(ctx, config = {}) {
|
|
|
150
205
|
description: 'Search emails by a keyword matched against sender, recipient and subject (server-side IMAP SEARCH). Body search is not supported by every server and is not attempted; returns the same compact rows as email_list.',
|
|
151
206
|
parameters: {
|
|
152
207
|
query: { type: 'string', required: true, description: 'Keyword to search for' },
|
|
153
|
-
folder: { type: 'string', description: 'IMAP folder to search in; defaults to the
|
|
208
|
+
folder: { type: 'string', description: 'IMAP folder to search in; defaults to the account inboxFolder' },
|
|
154
209
|
limit: { type: 'integer', description: 'How many matches to return, 1-100, default 10' },
|
|
210
|
+
account: { type: 'string', description: ACCOUNT_HINT },
|
|
155
211
|
},
|
|
156
212
|
output: {
|
|
157
213
|
schema: listSchema,
|
|
@@ -159,21 +215,22 @@ export function apply(ctx, config = {}) {
|
|
|
159
215
|
},
|
|
160
216
|
async execute(rawArgs) {
|
|
161
217
|
const args = rawArgs;
|
|
162
|
-
const cfg = resolve();
|
|
163
218
|
if (typeof args.query !== 'string' || args.query.trim() === '')
|
|
164
219
|
throw new Error('query 不能为空');
|
|
165
220
|
const limit = clampInt(args.limit, 10, 1, MAX_LIMIT);
|
|
166
|
-
return await
|
|
221
|
+
return await getPool().search(args.account, args.query.trim(), args.folder?.trim() || '', limit);
|
|
167
222
|
},
|
|
168
223
|
});
|
|
169
224
|
ctx.tools.register({
|
|
170
225
|
name: 'email_send',
|
|
171
|
-
description: 'Send an email from
|
|
226
|
+
description: 'Send an email from a configured account, optionally with file attachments (absolute paths, or relative to the dsh process cwd). Sending first asks the user for approval (recipient, subject and attachment count are shown) unless sendApproval is disabled; never invent recipients or content without the user\'s instruction.',
|
|
172
227
|
parameters: {
|
|
173
228
|
to: { type: 'string', required: true, description: 'Recipient(s), comma-separated' },
|
|
174
229
|
subject: { type: 'string', required: true, description: 'Email subject' },
|
|
175
230
|
text: { type: 'string', description: 'Plain-text body' },
|
|
176
231
|
cc: { type: 'string', description: 'CC recipient(s), comma-separated' },
|
|
232
|
+
attachments: { type: 'array', items: { type: 'string' }, description: 'File paths to attach (absolute, or relative to the dsh process cwd)' },
|
|
233
|
+
account: { type: 'string', description: ACCOUNT_HINT },
|
|
177
234
|
},
|
|
178
235
|
output: {
|
|
179
236
|
schema: sendSchema,
|
|
@@ -181,36 +238,68 @@ export function apply(ctx, config = {}) {
|
|
|
181
238
|
},
|
|
182
239
|
async execute(rawArgs) {
|
|
183
240
|
const args = rawArgs;
|
|
184
|
-
const cfg = resolve();
|
|
185
241
|
if (typeof args.to !== 'string' || args.to.trim() === '')
|
|
186
242
|
throw new Error('to 不能为空');
|
|
187
243
|
if (typeof args.subject !== 'string' || args.subject.trim() === '')
|
|
188
244
|
throw new Error('subject 不能为空');
|
|
189
|
-
return await
|
|
245
|
+
return await getPool().send(args.account, args.to.trim(), args.subject.trim(), typeof args.text === 'string' ? args.text : undefined, typeof args.cc === 'string' && args.cc.trim() !== '' ? args.cc.trim() : undefined, Array.isArray(args.attachments) ? args.attachments : undefined);
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
ctx.tools.register({
|
|
249
|
+
name: 'email_folders',
|
|
250
|
+
description: 'List the mailbox folders of an account (INBOX, Sent, Trash, custom folders, ...). Use the returned path values as the folder argument of the other email tools.',
|
|
251
|
+
parameters: {
|
|
252
|
+
subscribedOnly: { type: 'boolean', description: 'Only subscribed folders, default false' },
|
|
253
|
+
account: { type: 'string', description: ACCOUNT_HINT },
|
|
254
|
+
},
|
|
255
|
+
output: {
|
|
256
|
+
schema: foldersSchema,
|
|
257
|
+
render: (_args, value) => renderFolders(value),
|
|
258
|
+
},
|
|
259
|
+
async execute(rawArgs) {
|
|
260
|
+
const args = rawArgs;
|
|
261
|
+
return await getPool().folders(args.account, args.subscribedOnly === true);
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
ctx.tools.register({
|
|
265
|
+
name: 'email_attachment',
|
|
266
|
+
description: 'Download one attachment of a message to a local file (size capped by maxAttachmentBytes). The index matches the attachments array of email_read. Returns the absolute path of the written file.',
|
|
267
|
+
parameters: {
|
|
268
|
+
uid: { type: 'integer', required: true, description: 'Message uid from email_list or email_search' },
|
|
269
|
+
index: { type: 'integer', description: '0-based attachment index, as listed by email_read; default 0' },
|
|
270
|
+
folder: { type: 'string', description: 'IMAP folder the uid belongs to; defaults to the account inboxFolder' },
|
|
271
|
+
account: { type: 'string', description: ACCOUNT_HINT },
|
|
272
|
+
},
|
|
273
|
+
output: {
|
|
274
|
+
schema: attachmentSchema,
|
|
275
|
+
render: (_args, value) => renderAttachment(value),
|
|
276
|
+
},
|
|
277
|
+
async execute(rawArgs) {
|
|
278
|
+
const args = rawArgs;
|
|
279
|
+
if (typeof args.uid !== 'number' || !Number.isInteger(args.uid) || args.uid <= 0) {
|
|
280
|
+
throw new Error('uid 必须是正整数(用 email_list 获取)');
|
|
281
|
+
}
|
|
282
|
+
const index = clampInt(args.index, 0, 0, 999);
|
|
283
|
+
return await getPool().downloadAttachment(args.account, args.folder?.trim() || '', args.uid, index);
|
|
190
284
|
},
|
|
191
285
|
});
|
|
192
286
|
// Approval gate: the user must confirm every send (recipient + subject).
|
|
193
|
-
// Runs before other listeners; degrades to
|
|
194
|
-
//
|
|
287
|
+
// Runs before other listeners; degrades to the tool-time failure only
|
|
288
|
+
// when the account itself is not configured yet.
|
|
195
289
|
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
196
290
|
if (exec?.name !== 'email_send')
|
|
197
291
|
return next();
|
|
198
292
|
if (config.sendApproval === false)
|
|
199
293
|
return next();
|
|
200
|
-
|
|
201
|
-
resolve();
|
|
202
|
-
}
|
|
203
|
-
catch {
|
|
294
|
+
if (settingsError !== '')
|
|
204
295
|
return next();
|
|
205
|
-
}
|
|
206
296
|
const args = (exec.args ?? {});
|
|
207
|
-
|
|
297
|
+
const attachCount = Array.isArray(args.attachments) ? args.attachments.length : 0;
|
|
298
|
+
const reason = '发送邮件给 ' + args.to + ',主题「' + args.subject + '」' + (attachCount > 0 ? ',附件 ' + attachCount + ' 个' : '');
|
|
299
|
+
return { kind: 'ask', reason };
|
|
208
300
|
}, { prepend: true });
|
|
209
301
|
}
|
|
210
|
-
function resolveSafeFolder(config) {
|
|
211
|
-
return config.inboxFolder?.trim() || 'INBOX';
|
|
212
|
-
}
|
|
213
302
|
export { PROVIDER_NAMES, EMAIL_PASSWORD_ENV } from './config.js';
|
|
214
|
-
export { resolveEmailConfig, clampInt } from './config.js';
|
|
215
|
-
export { stripHtml, truncateText, flattenAddresses, parseRawMessage } from './parse.js';
|
|
216
|
-
export {
|
|
303
|
+
export { resolveEmailConfig, resolveEmailSettings, clampInt, defaultDownloadDir } from './config.js';
|
|
304
|
+
export { stripHtml, truncateText, flattenAddresses, sanitizeFilename, parseRawMessage } from './parse.js';
|
|
305
|
+
export { EmailPool, MailError, messageOf, validateAttachmentPaths } from './mail-client.js';
|
package/lib/mail-client.d.ts
CHANGED
|
@@ -1,20 +1,43 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type {
|
|
1
|
+
import { ImapFlow } from 'imapflow';
|
|
2
|
+
import type { ResolvedEmailConfig, ResolvedEmailSettings } from './config.js';
|
|
3
|
+
import type { EmailAttachmentResult, EmailFoldersResult, EmailListResult, EmailReadResult, EmailSearchResult, EmailSendResult } from './types.js';
|
|
3
4
|
export declare class MailError extends Error {
|
|
4
5
|
constructor(message: string);
|
|
5
6
|
}
|
|
6
7
|
export declare function messageOf(error: unknown, fallback: string): string;
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
/**
|
|
9
|
+
* One mailbox pool for the whole plugin: pooled IMAP connections per
|
|
10
|
+
* account plus pooled SMTP transporters, with idle sweep and error eviction.
|
|
11
|
+
*/
|
|
12
|
+
export declare class EmailPool {
|
|
13
|
+
private readonly settings;
|
|
14
|
+
private readonly imaps;
|
|
15
|
+
private readonly smtps;
|
|
16
|
+
private readonly queues;
|
|
17
|
+
private idleTimer;
|
|
18
|
+
constructor(settings: ResolvedEmailSettings);
|
|
19
|
+
account(name: string): ResolvedEmailConfig;
|
|
20
|
+
resolveName(name?: string): string;
|
|
21
|
+
/** Serialize operations per account: one IMAP connection serves one op at a time. */
|
|
22
|
+
private enqueue;
|
|
23
|
+
withImap<T>(accountName: string | undefined, folder: string | null, run: (client: ImapFlow) => Promise<T>): Promise<T>;
|
|
11
24
|
private createImap;
|
|
12
|
-
|
|
13
|
-
private
|
|
14
|
-
private
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
private imapRun;
|
|
26
|
+
private normalizeImapError;
|
|
27
|
+
private evictImap;
|
|
28
|
+
/** Reap IMAP connections idle for longer than idleTimeoutMs. */
|
|
29
|
+
startIdleSweep(): void;
|
|
30
|
+
dispose(): void;
|
|
31
|
+
private transporter;
|
|
32
|
+
list(accountName: string | undefined, folder: string, limit: number, offset: number, unreadOnly: boolean): Promise<EmailListResult>;
|
|
33
|
+
search(accountName: string | undefined, query: string, folder: string, limit: number): Promise<EmailSearchResult>;
|
|
17
34
|
private fetchListed;
|
|
18
|
-
read(uid: number, folder: string): Promise<EmailReadResult>;
|
|
19
|
-
|
|
35
|
+
read(accountName: string | undefined, uid: number, folder: string): Promise<EmailReadResult>;
|
|
36
|
+
folders(accountName: string | undefined, subscribedOnly: boolean): Promise<EmailFoldersResult>;
|
|
37
|
+
downloadAttachment(accountName: string | undefined, folder: string, uid: number, index: number): Promise<EmailAttachmentResult>;
|
|
38
|
+
send(accountName: string | undefined, to: string, subject: string, text: string | undefined, cc: string | undefined, attachmentPaths: string[] | undefined): Promise<EmailSendResult>;
|
|
20
39
|
}
|
|
40
|
+
/** Stat every attachment path up front; total size must stay under the cap. */
|
|
41
|
+
export declare function validateAttachmentPaths(paths: string[], maxBytes: number): Promise<Array<{
|
|
42
|
+
path: string;
|
|
43
|
+
}>>;
|