@lmcl/ailo-mcp-email 0.0.1
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 +45 -0
- package/dist/email-handler.js +486 -0
- package/dist/index.js +33 -0
- package/dist/mcp-server.js +194 -0
- package/package.json +35 -0
- package/src/email-handler.ts +565 -0
- package/src/index.ts +39 -0
- package/src/mcp-server.ts +209 -0
- package/src/types.d.ts +51 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 邮件通道 MCP:单一 email 工具,通过 action 参数分发所有能力。
|
|
3
|
+
*/
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
const actionSchema = z.enum([
|
|
7
|
+
"send",
|
|
8
|
+
"reply",
|
|
9
|
+
"forward",
|
|
10
|
+
"list",
|
|
11
|
+
"read",
|
|
12
|
+
"search",
|
|
13
|
+
"mark_read",
|
|
14
|
+
"move",
|
|
15
|
+
"delete",
|
|
16
|
+
"get_attachment",
|
|
17
|
+
]);
|
|
18
|
+
function ok(text) {
|
|
19
|
+
return { content: [{ type: "text", text }], isError: false };
|
|
20
|
+
}
|
|
21
|
+
function err(text) {
|
|
22
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
23
|
+
}
|
|
24
|
+
export function createEmailMcpServer(handler) {
|
|
25
|
+
const server = new McpServer({ name: "email", version: "0.1.0" });
|
|
26
|
+
server.registerTool("email", {
|
|
27
|
+
description: `邮件操作统一入口。action 取值:
|
|
28
|
+
- send: 发送新邮件(to, body, subject?, cc?, bcc?, html?, attachments?)
|
|
29
|
+
- reply: 回复邮件(uid, body, folder?, html?, attachments?)
|
|
30
|
+
- forward: 转发邮件(uid, to, folder?, cc?, bcc?, body?)
|
|
31
|
+
- list: 邮件列表(folder?, limit?, offset?, unread_only?)
|
|
32
|
+
- read: 读取单封详情(uid, folder?)
|
|
33
|
+
- search: 搜索(query?, from?, to?, subject?, since?, until?, folder?, limit?)
|
|
34
|
+
- mark_read: 标记已读/未读(uids, read, folder?)
|
|
35
|
+
- move: 移动邮件(uids, folder, from_folder?)
|
|
36
|
+
- delete: 删除邮件(uids, folder?)
|
|
37
|
+
- get_attachment: 下载附件 base64(uid, filename, folder?)`,
|
|
38
|
+
inputSchema: {
|
|
39
|
+
action: actionSchema,
|
|
40
|
+
to: z.string().optional(),
|
|
41
|
+
cc: z.string().optional(),
|
|
42
|
+
bcc: z.string().optional(),
|
|
43
|
+
subject: z.string().optional(),
|
|
44
|
+
body: z.string().optional(),
|
|
45
|
+
html: z.string().optional(),
|
|
46
|
+
uid: z.number().optional(),
|
|
47
|
+
uids: z.array(z.number()).max(500).optional(),
|
|
48
|
+
folder: z.string().optional(),
|
|
49
|
+
from_folder: z.string().optional(),
|
|
50
|
+
limit: z.number().min(1).max(200).optional(),
|
|
51
|
+
offset: z.number().min(0).optional(),
|
|
52
|
+
unread_only: z.boolean().optional(),
|
|
53
|
+
read: z.boolean().optional(),
|
|
54
|
+
query: z.string().optional(),
|
|
55
|
+
from: z.string().optional(),
|
|
56
|
+
to_search: z.string().optional(),
|
|
57
|
+
since: z.string().optional(),
|
|
58
|
+
until: z.string().optional(),
|
|
59
|
+
filename: z.string().optional(),
|
|
60
|
+
attachments: z
|
|
61
|
+
.array(z.object({
|
|
62
|
+
filename: z.string(),
|
|
63
|
+
content: z.string(),
|
|
64
|
+
contentType: z.string().optional(),
|
|
65
|
+
}))
|
|
66
|
+
.optional(),
|
|
67
|
+
},
|
|
68
|
+
}, async (args) => {
|
|
69
|
+
try {
|
|
70
|
+
const a = args.action;
|
|
71
|
+
switch (a) {
|
|
72
|
+
case "send": {
|
|
73
|
+
if (!args.to || args.body === undefined)
|
|
74
|
+
return err("send 需要 to 和 body");
|
|
75
|
+
await handler.send({
|
|
76
|
+
to: args.to,
|
|
77
|
+
cc: args.cc,
|
|
78
|
+
bcc: args.bcc,
|
|
79
|
+
subject: args.subject,
|
|
80
|
+
body: args.body,
|
|
81
|
+
html: args.html,
|
|
82
|
+
attachments: args.attachments,
|
|
83
|
+
});
|
|
84
|
+
return ok(`邮件已发送至 ${args.to}`);
|
|
85
|
+
}
|
|
86
|
+
case "reply": {
|
|
87
|
+
if (args.uid === undefined || args.body === undefined)
|
|
88
|
+
return err("reply 需要 uid 和 body");
|
|
89
|
+
await handler.reply({
|
|
90
|
+
uid: args.uid,
|
|
91
|
+
folder: args.folder,
|
|
92
|
+
body: args.body,
|
|
93
|
+
html: args.html,
|
|
94
|
+
attachments: args.attachments,
|
|
95
|
+
});
|
|
96
|
+
return ok(`已回复 uid=${args.uid}`);
|
|
97
|
+
}
|
|
98
|
+
case "forward": {
|
|
99
|
+
if (args.uid === undefined || !args.to)
|
|
100
|
+
return err("forward 需要 uid 和 to");
|
|
101
|
+
await handler.forward({
|
|
102
|
+
uid: args.uid,
|
|
103
|
+
folder: args.folder,
|
|
104
|
+
to: args.to,
|
|
105
|
+
cc: args.cc,
|
|
106
|
+
bcc: args.bcc,
|
|
107
|
+
body: args.body,
|
|
108
|
+
});
|
|
109
|
+
return ok(`已转发至 ${args.to}`);
|
|
110
|
+
}
|
|
111
|
+
case "list": {
|
|
112
|
+
const items = await handler.list({
|
|
113
|
+
folder: args.folder,
|
|
114
|
+
limit: args.limit,
|
|
115
|
+
offset: args.offset,
|
|
116
|
+
unreadOnly: args.unread_only,
|
|
117
|
+
});
|
|
118
|
+
const lines = items.map((i) => `uid=${i.uid} ${i.isRead ? "✓" : "○"} ${i.from} | ${i.subject} | ${i.date}`);
|
|
119
|
+
return ok(lines.length ? lines.join("\n") : "(无邮件)");
|
|
120
|
+
}
|
|
121
|
+
case "read": {
|
|
122
|
+
if (args.uid === undefined)
|
|
123
|
+
return err("read 需要 uid");
|
|
124
|
+
const d = await handler.read({ uid: args.uid, folder: args.folder });
|
|
125
|
+
if (!d)
|
|
126
|
+
return err(`uid=${args.uid} 不存在`);
|
|
127
|
+
const att = d.attachments.length
|
|
128
|
+
? `\n附件: ${d.attachments.map((a) => `${a.filename} (${a.size}B)`).join(", ")}`
|
|
129
|
+
: "";
|
|
130
|
+
return ok(`from: ${d.from}\nto: ${d.to}\nsubject: ${d.subject}\ndate: ${d.date}\n\n${d.text ?? d.html ?? ""}${att}`);
|
|
131
|
+
}
|
|
132
|
+
case "search": {
|
|
133
|
+
const items = await handler.search({
|
|
134
|
+
query: args.query,
|
|
135
|
+
from: args.from,
|
|
136
|
+
to: args.to_search,
|
|
137
|
+
subject: args.subject,
|
|
138
|
+
since: args.since,
|
|
139
|
+
until: args.until,
|
|
140
|
+
folder: args.folder,
|
|
141
|
+
limit: args.limit,
|
|
142
|
+
});
|
|
143
|
+
const lines = items.map((i) => `uid=${i.uid} ${i.isRead ? "✓" : "○"} ${i.from} | ${i.subject} | ${i.date}`);
|
|
144
|
+
return ok(lines.length ? lines.join("\n") : "(无匹配)");
|
|
145
|
+
}
|
|
146
|
+
case "mark_read": {
|
|
147
|
+
if (!args.uids?.length || args.read === undefined)
|
|
148
|
+
return err("mark_read 需要 uids 和 read");
|
|
149
|
+
await handler.markRead({
|
|
150
|
+
uids: args.uids,
|
|
151
|
+
read: args.read,
|
|
152
|
+
folder: args.folder,
|
|
153
|
+
});
|
|
154
|
+
return ok(`已标记 ${args.uids.length} 封为${args.read ? "已读" : "未读"}`);
|
|
155
|
+
}
|
|
156
|
+
case "move": {
|
|
157
|
+
if (!args.uids?.length || !args.folder)
|
|
158
|
+
return err("move 需要 uids 和 folder");
|
|
159
|
+
await handler.move({
|
|
160
|
+
uids: args.uids,
|
|
161
|
+
folder: args.folder,
|
|
162
|
+
fromFolder: args.from_folder,
|
|
163
|
+
});
|
|
164
|
+
return ok(`已移动 ${args.uids.length} 封到 ${args.folder}`);
|
|
165
|
+
}
|
|
166
|
+
case "delete": {
|
|
167
|
+
if (!args.uids?.length)
|
|
168
|
+
return err("delete 需要 uids");
|
|
169
|
+
await handler.delete({ uids: args.uids, folder: args.folder });
|
|
170
|
+
return ok(`已删除 ${args.uids.length} 封`);
|
|
171
|
+
}
|
|
172
|
+
case "get_attachment": {
|
|
173
|
+
if (args.uid === undefined || !args.filename)
|
|
174
|
+
return err("get_attachment 需要 uid 和 filename");
|
|
175
|
+
const b64 = await handler.getAttachment({
|
|
176
|
+
uid: args.uid,
|
|
177
|
+
folder: args.folder,
|
|
178
|
+
filename: args.filename,
|
|
179
|
+
});
|
|
180
|
+
if (!b64)
|
|
181
|
+
return err(`附件 ${args.filename} 不存在`);
|
|
182
|
+
return ok(`base64:${b64}`);
|
|
183
|
+
}
|
|
184
|
+
default:
|
|
185
|
+
return err(`未知 action: ${a}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
190
|
+
return err(`操作失败: ${msg}`);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
return server;
|
|
194
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lmcl/ailo-mcp-email",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Ailo 邮件通道 MCP - IMAP 收信 + SMTP 发信",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ailo-mcp-email": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "tsx src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@lmcl/ailo-mcp-sdk": "^0.0.2",
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
18
|
+
"dotenv": "^16.4.5",
|
|
19
|
+
"imap": "^0.8.19",
|
|
20
|
+
"mailparser": "^3.6.6",
|
|
21
|
+
"nodemailer": "^6.9.16",
|
|
22
|
+
"zod": "^4.3.6"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/imap": "^0.8.40",
|
|
26
|
+
"@types/mailparser": "^3.4.5",
|
|
27
|
+
"@types/node": "^22.10.0",
|
|
28
|
+
"@types/nodemailer": "^6.4.17",
|
|
29
|
+
"tsx": "^4.19.0",
|
|
30
|
+
"typescript": "^5.7.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
}
|
|
35
|
+
}
|