fluxmail 0.0.1 → 0.1.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/accounts/googleAuth.d.ts +31 -0
- package/dist/accounts/googleAuth.d.ts.map +1 -0
- package/dist/accounts/googleAuth.js +143 -0
- package/dist/accounts/googleAuth.js.map +1 -0
- package/dist/accounts/registry.d.ts +28 -0
- package/dist/accounts/registry.d.ts.map +1 -0
- package/dist/accounts/registry.js +185 -0
- package/dist/accounts/registry.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +434 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +40 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +215 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +15 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +15 -0
- package/dist/context.js.map +1 -0
- package/dist/http/app.d.ts +16 -0
- package/dist/http/app.d.ts.map +1 -0
- package/dist/http/app.js +110 -0
- package/dist/http/app.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/licensing/activation.d.ts +5 -0
- package/dist/licensing/activation.d.ts.map +1 -0
- package/dist/licensing/activation.js +11 -0
- package/dist/licensing/activation.js.map +1 -0
- package/dist/licensing/client.d.ts +52 -0
- package/dist/licensing/client.d.ts.map +1 -0
- package/dist/licensing/client.js +75 -0
- package/dist/licensing/client.js.map +1 -0
- package/dist/licensing/entitlements.d.ts +55 -0
- package/dist/licensing/entitlements.d.ts.map +1 -0
- package/dist/licensing/entitlements.js +116 -0
- package/dist/licensing/entitlements.js.map +1 -0
- package/dist/licensing/lease.d.ts +47 -0
- package/dist/licensing/lease.d.ts.map +1 -0
- package/dist/licensing/lease.js +103 -0
- package/dist/licensing/lease.js.map +1 -0
- package/dist/licensing/refresher.d.ts +46 -0
- package/dist/licensing/refresher.d.ts.map +1 -0
- package/dist/licensing/refresher.js +150 -0
- package/dist/licensing/refresher.js.map +1 -0
- package/dist/mcp/buildServer.d.ts +28 -0
- package/dist/mcp/buildServer.d.ts.map +1 -0
- package/dist/mcp/buildServer.js +394 -0
- package/dist/mcp/buildServer.js.map +1 -0
- package/dist/scheduler/sendScheduler.d.ts +38 -0
- package/dist/scheduler/sendScheduler.d.ts.map +1 -0
- package/dist/scheduler/sendScheduler.js +130 -0
- package/dist/scheduler/sendScheduler.js.map +1 -0
- package/dist/service/emailService.d.ts +138 -0
- package/dist/service/emailService.d.ts.map +1 -0
- package/dist/service/emailService.js +375 -0
- package/dist/service/emailService.js.map +1 -0
- package/dist/storage/apiKeys.d.ts +27 -0
- package/dist/storage/apiKeys.d.ts.map +1 -0
- package/dist/storage/apiKeys.js +43 -0
- package/dist/storage/apiKeys.js.map +1 -0
- package/dist/storage/crypto.d.ts +4 -0
- package/dist/storage/crypto.d.ts.map +1 -0
- package/dist/storage/crypto.js +21 -0
- package/dist/storage/crypto.js.map +1 -0
- package/dist/storage/db.d.ts +729 -0
- package/dist/storage/db.d.ts.map +1 -0
- package/dist/storage/db.js +143 -0
- package/dist/storage/db.js.map +1 -0
- package/dist/storage/members.d.ts +29 -0
- package/dist/storage/members.d.ts.map +1 -0
- package/dist/storage/members.js +67 -0
- package/dist/storage/members.js.map +1 -0
- package/dist/storage/scheduledSends.d.ts +60 -0
- package/dist/storage/scheduledSends.d.ts.map +1 -0
- package/dist/storage/scheduledSends.js +169 -0
- package/dist/storage/scheduledSends.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/package.json +38 -15
- package/README.md +0 -29
- package/bin/fluxmail.js +0 -4
- package/src/cli.js +0 -36
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { existsSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { VERSION } from '../version.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import { EmailError, isEmailError, parseSingleAddress, } from '@fluxmail/core';
|
|
7
|
+
const MAX_BODY_CHARS = 50_000;
|
|
8
|
+
const MAX_INLINE_ATTACHMENT_BYTES = 2 * 1024 * 1024;
|
|
9
|
+
const accountIdParam = z
|
|
10
|
+
.string()
|
|
11
|
+
.min(1)
|
|
12
|
+
.optional()
|
|
13
|
+
.describe('Account to operate on. Optional when exactly one account is connected.');
|
|
14
|
+
const idParam = z.string().min(1);
|
|
15
|
+
const addressList = z
|
|
16
|
+
.array(z.string().min(1))
|
|
17
|
+
.describe('Recipients, each "Name <a@x.com>" or "a@x.com"');
|
|
18
|
+
const queryShape = {
|
|
19
|
+
folder: z.string().min(1).optional().describe('Folder role (inbox, sent, drafts, trash, spam, starred, archive, all) or a label/folder name'),
|
|
20
|
+
text: z.string().optional().describe('Full-text search terms'),
|
|
21
|
+
from: z.string().optional(),
|
|
22
|
+
to: z.string().optional(),
|
|
23
|
+
subject: z.string().optional(),
|
|
24
|
+
unreadOnly: z.boolean().optional(),
|
|
25
|
+
starredOnly: z.boolean().optional(),
|
|
26
|
+
hasAttachment: z.boolean().optional(),
|
|
27
|
+
after: z.string().min(1).optional().describe('ISO date, inclusive'),
|
|
28
|
+
before: z.string().min(1).optional().describe('ISO date, exclusive'),
|
|
29
|
+
rawProviderQuery: z.string().optional().describe('Escape hatch passed verbatim to the provider (e.g. Gmail q= syntax)'),
|
|
30
|
+
pageSize: z.number().int().min(1).max(100).optional().describe('Defaults to 25'),
|
|
31
|
+
pageToken: z.string().min(1).optional().describe('nextPageToken from a previous call'),
|
|
32
|
+
};
|
|
33
|
+
const draftShape = {
|
|
34
|
+
accountId: accountIdParam,
|
|
35
|
+
to: addressList.optional(),
|
|
36
|
+
cc: addressList.optional(),
|
|
37
|
+
bcc: addressList.optional(),
|
|
38
|
+
subject: z.string().optional().describe('Defaults to "Re: ..." when replying'),
|
|
39
|
+
bodyText: z.string().optional().describe('Plain-text body'),
|
|
40
|
+
bodyHtml: z.string().optional().describe('HTML body'),
|
|
41
|
+
replyToMessageId: idParam.optional().describe('Message being replied to; threads correctly and computes recipients if "to" is omitted'),
|
|
42
|
+
replyAll: z.boolean().optional().describe('With replyToMessageId: reply to all original recipients'),
|
|
43
|
+
attachments: z
|
|
44
|
+
.array(z.object({
|
|
45
|
+
filename: z.string().min(1),
|
|
46
|
+
mimeType: z.string().min(1),
|
|
47
|
+
content: z.string().describe('base64'),
|
|
48
|
+
}))
|
|
49
|
+
.optional(),
|
|
50
|
+
};
|
|
51
|
+
function parseAddresses(raw) {
|
|
52
|
+
if (!raw)
|
|
53
|
+
return undefined;
|
|
54
|
+
const parsed = raw.map((r) => {
|
|
55
|
+
const addr = parseSingleAddress(r);
|
|
56
|
+
if (!addr)
|
|
57
|
+
throw new EmailError('invalid_request', `Could not parse email address: "${r}"`);
|
|
58
|
+
return addr;
|
|
59
|
+
});
|
|
60
|
+
return parsed;
|
|
61
|
+
}
|
|
62
|
+
function toSendInput(args) {
|
|
63
|
+
if (args.replyAll && !args.replyToMessageId) {
|
|
64
|
+
throw new EmailError('invalid_request', 'replyAll requires replyToMessageId');
|
|
65
|
+
}
|
|
66
|
+
const input = {
|
|
67
|
+
body: {
|
|
68
|
+
...(args.bodyText !== undefined ? { text: args.bodyText } : {}),
|
|
69
|
+
...(args.bodyHtml !== undefined ? { html: args.bodyHtml } : {}),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
const to = parseAddresses(args.to);
|
|
73
|
+
if (to?.length)
|
|
74
|
+
input.to = to;
|
|
75
|
+
const cc = parseAddresses(args.cc);
|
|
76
|
+
if (cc?.length)
|
|
77
|
+
input.cc = cc;
|
|
78
|
+
const bcc = parseAddresses(args.bcc);
|
|
79
|
+
if (bcc?.length)
|
|
80
|
+
input.bcc = bcc;
|
|
81
|
+
if (args.subject !== undefined)
|
|
82
|
+
input.subject = args.subject;
|
|
83
|
+
if (args.replyToMessageId)
|
|
84
|
+
input.replyToMessageId = args.replyToMessageId;
|
|
85
|
+
if (args.replyAll !== undefined)
|
|
86
|
+
input.replyAll = args.replyAll;
|
|
87
|
+
if (args.attachments?.length)
|
|
88
|
+
input.attachments = args.attachments;
|
|
89
|
+
return input;
|
|
90
|
+
}
|
|
91
|
+
export function toSendRequest(args) {
|
|
92
|
+
if (args.draftId !== undefined) {
|
|
93
|
+
const contentKeys = [
|
|
94
|
+
'to',
|
|
95
|
+
'cc',
|
|
96
|
+
'bcc',
|
|
97
|
+
'subject',
|
|
98
|
+
'bodyText',
|
|
99
|
+
'bodyHtml',
|
|
100
|
+
'replyToMessageId',
|
|
101
|
+
'replyAll',
|
|
102
|
+
'attachments',
|
|
103
|
+
];
|
|
104
|
+
if (contentKeys.some((key) => args[key] !== undefined)) {
|
|
105
|
+
throw new EmailError('invalid_request', 'draftId cannot be combined with message content; update the draft before sending it');
|
|
106
|
+
}
|
|
107
|
+
return { draftId: args.draftId };
|
|
108
|
+
}
|
|
109
|
+
return toSendInput(args);
|
|
110
|
+
}
|
|
111
|
+
function truncateBody(message) {
|
|
112
|
+
if (!message.body)
|
|
113
|
+
return message;
|
|
114
|
+
const body = { ...message.body };
|
|
115
|
+
for (const key of ['text', 'html']) {
|
|
116
|
+
const value = body[key];
|
|
117
|
+
if (value && value.length > MAX_BODY_CHARS) {
|
|
118
|
+
body[key] = value.slice(0, MAX_BODY_CHARS) + `\n… [truncated ${value.length - MAX_BODY_CHARS} characters]`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { ...message, body };
|
|
122
|
+
}
|
|
123
|
+
function ok(data, warning) {
|
|
124
|
+
return {
|
|
125
|
+
content: [
|
|
126
|
+
{ type: 'text', text: JSON.stringify(data, null, 2) },
|
|
127
|
+
...(warning ? [{ type: 'text', text: `Note: ${warning}` }] : []),
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function toolError(err) {
|
|
132
|
+
const payload = isEmailError(err)
|
|
133
|
+
? { error: err.code, message: err.message, ...(err.data ? { data: err.data } : {}) }
|
|
134
|
+
: { error: 'internal', message: err instanceof Error ? err.message : String(err) };
|
|
135
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
136
|
+
}
|
|
137
|
+
function handle(fn, gate) {
|
|
138
|
+
return async (...args) => {
|
|
139
|
+
try {
|
|
140
|
+
// The gate throws when a lapsed license leaves the instance over quota,
|
|
141
|
+
// and yields a renewal warning to attach while the license is in grace.
|
|
142
|
+
const warning = gate?.();
|
|
143
|
+
return ok(await fn(...args), warning);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
return toolError(err);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function pageOpts(args) {
|
|
151
|
+
return {
|
|
152
|
+
...(args.pageSize !== undefined ? { pageSize: args.pageSize } : {}),
|
|
153
|
+
...(args.pageToken !== undefined ? { pageToken: args.pageToken } : {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export function resolveAttachmentSavePath(savePath, filename) {
|
|
157
|
+
const isDir = savePath.endsWith(path.sep) ||
|
|
158
|
+
(existsSync(savePath) && statSync(savePath).isDirectory());
|
|
159
|
+
if (!isDir)
|
|
160
|
+
return savePath;
|
|
161
|
+
const safeFilename = path.posix.basename(filename.replace(/\\/g, '/'));
|
|
162
|
+
if (!safeFilename || safeFilename === '.' || safeFilename === '..') {
|
|
163
|
+
throw new Error('Attachment filename is not safe to use');
|
|
164
|
+
}
|
|
165
|
+
const directory = path.resolve(savePath);
|
|
166
|
+
const target = path.resolve(directory, safeFilename);
|
|
167
|
+
if (path.dirname(target) !== directory) {
|
|
168
|
+
throw new Error('Attachment filename escapes the destination directory');
|
|
169
|
+
}
|
|
170
|
+
return target;
|
|
171
|
+
}
|
|
172
|
+
export function saveAttachment(savePath, filename, content) {
|
|
173
|
+
if (!path.isAbsolute(savePath))
|
|
174
|
+
throw new EmailError('invalid_request', 'savePath must be absolute');
|
|
175
|
+
const target = resolveAttachmentSavePath(savePath, filename);
|
|
176
|
+
try {
|
|
177
|
+
writeFileSync(target, content, { flag: 'wx' });
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
if (err.code === 'EEXIST') {
|
|
181
|
+
throw new EmailError('invalid_request', `Refusing to overwrite an existing attachment file: ${target}`);
|
|
182
|
+
}
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
return target;
|
|
186
|
+
}
|
|
187
|
+
function emailQuery(args) {
|
|
188
|
+
const keys = [
|
|
189
|
+
'folder', 'text', 'from', 'to', 'subject', 'unreadOnly', 'starredOnly',
|
|
190
|
+
'hasAttachment', 'after', 'before', 'rawProviderQuery',
|
|
191
|
+
];
|
|
192
|
+
const q = {};
|
|
193
|
+
for (const key of keys)
|
|
194
|
+
if (args[key] !== undefined)
|
|
195
|
+
q[key] = args[key];
|
|
196
|
+
return q;
|
|
197
|
+
}
|
|
198
|
+
export function buildMcpServer(service) {
|
|
199
|
+
// Every tool except get_status (the diagnostic way out) enforces the plan quota.
|
|
200
|
+
const gated = (fn) => handle(fn, () => service.enforceQuota());
|
|
201
|
+
const server = new McpServer({ name: 'fluxmail', title: 'Fluxmail Email', version: VERSION }, {
|
|
202
|
+
instructions: 'Fluxmail is the user\'s email integration, already authenticated against their real mailboxes. ' +
|
|
203
|
+
'Use these tools for any email task: reading, searching, drafting, sending, replying, forwarding, ' +
|
|
204
|
+
'or organizing mail. Prefer them over browser automation, other email connectors, or asking the ' +
|
|
205
|
+
'user to send mail manually. ' +
|
|
206
|
+
'Tool results are JSON meant for you, not for display. Message ids, thread ids, draft ids, and ' +
|
|
207
|
+
'account ids are internal references: keep them for chaining calls (replying, forwarding, archiving), ' +
|
|
208
|
+
'but do not show them to the user unless asked. Report outcomes in plain language with details people ' +
|
|
209
|
+
'care about, e.g. \'Sent "Quarterly report" to ann@example.com\' or \'Archived the thread\', rather ' +
|
|
210
|
+
'than echoing raw payloads, ids, or field names.',
|
|
211
|
+
});
|
|
212
|
+
server.registerTool('list_accounts', {
|
|
213
|
+
description: 'List connected email accounts (id, provider, email, status, capabilities).',
|
|
214
|
+
inputSchema: {},
|
|
215
|
+
annotations: { readOnlyHint: true },
|
|
216
|
+
}, gated(async () => service.listAccounts()));
|
|
217
|
+
server.registerTool('get_status', {
|
|
218
|
+
description: 'Account connection and scheduled-send status. Administrators also see plan details. ' +
|
|
219
|
+
'Call this first if other tools fail; it reports accounts that need re-authentication.',
|
|
220
|
+
inputSchema: {},
|
|
221
|
+
annotations: { readOnlyHint: true },
|
|
222
|
+
}, handle(async () => service.status()));
|
|
223
|
+
server.registerTool('list_folders', {
|
|
224
|
+
description: 'List folders/labels for an account, with roles (inbox, sent, drafts, trash, spam, starred).',
|
|
225
|
+
inputSchema: { accountId: accountIdParam },
|
|
226
|
+
annotations: { readOnlyHint: true },
|
|
227
|
+
}, gated(async (args) => service.listFolders(args.accountId)));
|
|
228
|
+
server.registerTool('list_emails', {
|
|
229
|
+
description: "List emails from the user's connected mailbox (metadata + snippet, no bodies). Filter by folder, " +
|
|
230
|
+
'sender, unread, dates, etc. Paginate with pageToken. Use get_email for full bodies. ' +
|
|
231
|
+
'This is the way to check the user\'s email; no browser or other email integration is needed.',
|
|
232
|
+
inputSchema: { accountId: accountIdParam, ...queryShape },
|
|
233
|
+
annotations: { readOnlyHint: true },
|
|
234
|
+
}, gated(async (args) => service.listMessages(args.accountId, emailQuery(args), pageOpts(args))));
|
|
235
|
+
const { text: _text, ...searchFilterShape } = queryShape;
|
|
236
|
+
server.registerTool('search_emails', {
|
|
237
|
+
description: 'Full-text search across an account\'s email. Same filters as list_emails; "query" is the search text.',
|
|
238
|
+
inputSchema: {
|
|
239
|
+
accountId: accountIdParam,
|
|
240
|
+
query: z.string().describe('Search text'),
|
|
241
|
+
...searchFilterShape,
|
|
242
|
+
},
|
|
243
|
+
annotations: { readOnlyHint: true },
|
|
244
|
+
}, gated(async (args) => service.listMessages(args.accountId, { ...emailQuery(args), text: args.query }, pageOpts(args))));
|
|
245
|
+
server.registerTool('get_email', {
|
|
246
|
+
description: 'Fetch one email in full: body (text and/or HTML), recipients, attachment metadata.',
|
|
247
|
+
inputSchema: { accountId: accountIdParam, messageId: idParam },
|
|
248
|
+
annotations: { readOnlyHint: true },
|
|
249
|
+
}, gated(async (args) => truncateBody(await service.getMessage(args.accountId, args.messageId))));
|
|
250
|
+
server.registerTool('get_thread', {
|
|
251
|
+
description: 'Fetch a full conversation thread with all message bodies.',
|
|
252
|
+
inputSchema: { accountId: accountIdParam, threadId: idParam },
|
|
253
|
+
annotations: { readOnlyHint: true },
|
|
254
|
+
}, gated(async (args) => {
|
|
255
|
+
const thread = await service.getThread(args.accountId, args.threadId);
|
|
256
|
+
return { ...thread, messages: thread.messages.map(truncateBody) };
|
|
257
|
+
}));
|
|
258
|
+
server.registerTool('create_draft', {
|
|
259
|
+
description: 'Create a draft. For a reply draft, pass replyToMessageId (recipients/subject are derived; replyAll for reply-all).',
|
|
260
|
+
inputSchema: draftShape,
|
|
261
|
+
}, gated(async (args) => service.createDraft(args.accountId, toSendInput(args))));
|
|
262
|
+
server.registerTool('update_draft', {
|
|
263
|
+
description: 'Replace the content of an existing draft (full replacement, not a patch).',
|
|
264
|
+
inputSchema: { draftId: idParam, ...draftShape },
|
|
265
|
+
}, gated(async (args) => service.updateDraft(args.accountId, args.draftId, toSendInput(args))));
|
|
266
|
+
server.registerTool('delete_draft', {
|
|
267
|
+
description: 'Delete a draft.',
|
|
268
|
+
inputSchema: { accountId: accountIdParam, draftId: idParam },
|
|
269
|
+
annotations: { destructiveHint: true },
|
|
270
|
+
}, gated(async (args) => {
|
|
271
|
+
await service.deleteDraft(args.accountId, args.draftId);
|
|
272
|
+
return { deleted: args.draftId };
|
|
273
|
+
}));
|
|
274
|
+
server.registerTool('send_email', {
|
|
275
|
+
description: "Send an email from the user's connected account; this actually delivers mail, so prefer it over " +
|
|
276
|
+
'browser automation or leaving a draft when the user asked to send. Three modes: direct (to + subject ' +
|
|
277
|
+
'+ body), sending an existing draft (draftId), or replying (replyToMessageId, optionally replyAll) ' +
|
|
278
|
+
'where recipients, subject, and threading are derived from the original. Confirm with the user when ' +
|
|
279
|
+
'intent is ambiguous. Add sendAt to any mode to schedule instead of sending now.',
|
|
280
|
+
inputSchema: {
|
|
281
|
+
draftId: idParam.optional().describe('Send this existing draft'),
|
|
282
|
+
...draftShape,
|
|
283
|
+
sendAt: z
|
|
284
|
+
.string()
|
|
285
|
+
.datetime({ offset: true })
|
|
286
|
+
.optional()
|
|
287
|
+
.describe('Schedule delivery instead of sending now: ISO 8601 with timezone offset or Z ' +
|
|
288
|
+
'(e.g. 2026-07-11T09:00:00-07:00). Fluxmail saves the message as a real draft in the mailbox ' +
|
|
289
|
+
'and sends it at this time; the server must be running then (anything missed while it was ' +
|
|
290
|
+
'down goes out at the next startup). Returns a scheduleId for list/cancel.'),
|
|
291
|
+
},
|
|
292
|
+
annotations: { destructiveHint: true },
|
|
293
|
+
}, gated(async (args) => {
|
|
294
|
+
const { sendAt, ...sendArgs } = args;
|
|
295
|
+
return sendAt !== undefined
|
|
296
|
+
? service.scheduleSend(args.accountId, toSendRequest(sendArgs), sendAt)
|
|
297
|
+
: service.send(args.accountId, toSendRequest(sendArgs));
|
|
298
|
+
}));
|
|
299
|
+
server.registerTool('list_scheduled_emails', {
|
|
300
|
+
description: 'List scheduled sends: pending ones first (with sendAt), then past ones (sent, failed, canceled). ' +
|
|
301
|
+
'For failed entries, lastError says what went wrong. Pending sends only fire while the ' +
|
|
302
|
+
'Fluxmail server is running.',
|
|
303
|
+
inputSchema: { accountId: accountIdParam },
|
|
304
|
+
annotations: { readOnlyHint: true },
|
|
305
|
+
}, gated(async (args) => service.listScheduled(args.accountId)));
|
|
306
|
+
server.registerTool('cancel_scheduled_email', {
|
|
307
|
+
description: 'Cancel a pending scheduled send by scheduleId (from send_email with sendAt, or list_scheduled_emails). ' +
|
|
308
|
+
'The draft stays in the Drafts folder, so the content is not lost.',
|
|
309
|
+
inputSchema: { scheduleId: idParam },
|
|
310
|
+
}, gated(async (args) => service.cancelScheduled(args.scheduleId)));
|
|
311
|
+
server.registerTool('forward_email', {
|
|
312
|
+
description: 'Forward an email to new recipients: quoted original body, "Fwd:" subject, original attachments included ' +
|
|
313
|
+
'unless includeAttachments=false. Optional comment appears above the forwarded content.',
|
|
314
|
+
inputSchema: {
|
|
315
|
+
accountId: accountIdParam,
|
|
316
|
+
messageId: idParam,
|
|
317
|
+
to: addressList.min(1),
|
|
318
|
+
cc: addressList.optional(),
|
|
319
|
+
comment: z.string().optional(),
|
|
320
|
+
includeAttachments: z.boolean().optional().describe('Default true'),
|
|
321
|
+
},
|
|
322
|
+
annotations: { destructiveHint: true },
|
|
323
|
+
}, gated(async (args) => {
|
|
324
|
+
const to = parseAddresses(args.to) ?? [];
|
|
325
|
+
const cc = parseAddresses(args.cc);
|
|
326
|
+
return service.forward(args.accountId, {
|
|
327
|
+
messageId: args.messageId,
|
|
328
|
+
to,
|
|
329
|
+
...(cc?.length ? { cc } : {}),
|
|
330
|
+
...(args.comment !== undefined ? { comment: args.comment } : {}),
|
|
331
|
+
...(args.includeAttachments !== undefined ? { includeAttachments: args.includeAttachments } : {}),
|
|
332
|
+
});
|
|
333
|
+
}));
|
|
334
|
+
server.registerTool('modify_emails', {
|
|
335
|
+
description: 'Batch-modify emails: markRead, markUnread, star, unstar, archive, trash, untrash, delete (permanent!), ' +
|
|
336
|
+
'move (requires folder), addLabels/removeLabels (requires labels; Gmail only).',
|
|
337
|
+
inputSchema: {
|
|
338
|
+
accountId: accountIdParam,
|
|
339
|
+
messageIds: z.array(idParam).min(1),
|
|
340
|
+
action: z.enum([
|
|
341
|
+
'markRead', 'markUnread', 'star', 'unstar', 'archive',
|
|
342
|
+
'trash', 'untrash', 'delete', 'move', 'addLabels', 'removeLabels',
|
|
343
|
+
]),
|
|
344
|
+
folder: z.string().min(1).optional().describe('Target folder for action=move'),
|
|
345
|
+
labels: z
|
|
346
|
+
.array(z.string().min(1))
|
|
347
|
+
.max(100)
|
|
348
|
+
.optional()
|
|
349
|
+
.describe('Labels for addLabels/removeLabels'),
|
|
350
|
+
},
|
|
351
|
+
annotations: { destructiveHint: true },
|
|
352
|
+
}, gated(async (args) => {
|
|
353
|
+
let action;
|
|
354
|
+
if (args.action === 'move') {
|
|
355
|
+
if (!args.folder)
|
|
356
|
+
throw new EmailError('invalid_request', 'action=move requires "folder"');
|
|
357
|
+
action = { move: args.folder };
|
|
358
|
+
}
|
|
359
|
+
else if (args.action === 'addLabels' || args.action === 'removeLabels') {
|
|
360
|
+
if (!args.labels?.length) {
|
|
361
|
+
throw new EmailError('invalid_request', `action=${args.action} requires "labels"`);
|
|
362
|
+
}
|
|
363
|
+
action = args.action === 'addLabels' ? { addLabels: args.labels } : { removeLabels: args.labels };
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
action = args.action;
|
|
367
|
+
}
|
|
368
|
+
await service.modify(args.accountId, args.messageIds, action);
|
|
369
|
+
return { modified: args.messageIds.length, action: args.action };
|
|
370
|
+
}));
|
|
371
|
+
server.registerTool('download_attachment', {
|
|
372
|
+
description: 'Download an email attachment. With savePath, writes the file to disk and returns the path; ' +
|
|
373
|
+
'otherwise returns base64 content (up to 2 MB).',
|
|
374
|
+
inputSchema: {
|
|
375
|
+
accountId: accountIdParam,
|
|
376
|
+
messageId: idParam,
|
|
377
|
+
attachmentId: idParam,
|
|
378
|
+
savePath: z.string().min(1).optional().describe('Absolute file path or directory to write the attachment to'),
|
|
379
|
+
},
|
|
380
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
381
|
+
}, gated(async (args) => {
|
|
382
|
+
const { meta, content } = await service.getAttachment(args.accountId, args.messageId, args.attachmentId);
|
|
383
|
+
if (args.savePath) {
|
|
384
|
+
const target = saveAttachment(args.savePath, meta.filename, content);
|
|
385
|
+
return { saved: target, ...meta };
|
|
386
|
+
}
|
|
387
|
+
if (content.length > MAX_INLINE_ATTACHMENT_BYTES) {
|
|
388
|
+
throw new EmailError('invalid_request', `Attachment is ${content.length} bytes (limit ${MAX_INLINE_ATTACHMENT_BYTES} inline). Pass savePath to write it to disk.`);
|
|
389
|
+
}
|
|
390
|
+
return { ...meta, contentBase64: content.toString('base64') };
|
|
391
|
+
}));
|
|
392
|
+
return server;
|
|
393
|
+
}
|
|
394
|
+
//# sourceMappingURL=buildServer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildServer.js","sourceRoot":"","sources":["../../src/mcp/buildServer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type SendResult } from '@fluxmail/core';
|
|
2
|
+
import type { FluxmailDb } from '../storage/db.js';
|
|
3
|
+
/** The slice of EmailService the scheduler needs. */
|
|
4
|
+
export interface ScheduledSender {
|
|
5
|
+
send(accountId: string | undefined, input: {
|
|
6
|
+
draftId: string;
|
|
7
|
+
}): Promise<SendResult>;
|
|
8
|
+
/** Throws while a lapsed license leaves the instance over the plan quota. */
|
|
9
|
+
enforceQuota(): string | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Fires scheduled sends when their time comes. Owned by the long-lived
|
|
13
|
+
* processes (serve/stdio); one-shot CLI commands construct it but never
|
|
14
|
+
* start() it. Assumes a single long-lived process per database; there is
|
|
15
|
+
* no cross-process locking.
|
|
16
|
+
*/
|
|
17
|
+
export declare class SendScheduler {
|
|
18
|
+
private readonly db;
|
|
19
|
+
private readonly service;
|
|
20
|
+
private timer;
|
|
21
|
+
private running;
|
|
22
|
+
private processing;
|
|
23
|
+
private rerun;
|
|
24
|
+
/** Retry gates for transient failures; in-memory only (a restart just retries immediately). */
|
|
25
|
+
private backoff;
|
|
26
|
+
/** While over the plan quota, due sends are held and re-checked no sooner than this. */
|
|
27
|
+
private quotaHoldUntil;
|
|
28
|
+
constructor(db: FluxmailDb, service: ScheduledSender);
|
|
29
|
+
/** Starts the loop; immediately fires anything past due (catch-up after downtime). */
|
|
30
|
+
start(): void;
|
|
31
|
+
stop(): void;
|
|
32
|
+
/** Poke after any schedule change (new, canceled, or sent early). */
|
|
33
|
+
wake(): void;
|
|
34
|
+
private tick;
|
|
35
|
+
private fire;
|
|
36
|
+
private arm;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=sendScheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sendScheduler.d.ts","sourceRoot":"","sources":["../../src/scheduler/sendScheduler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAYnD,qDAAqD;AACrD,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACrF,6EAA6E;IAC7E,YAAY,IAAI,MAAM,GAAG,SAAS,CAAC;CACpC;AAUD;;;;;GAKG;AACH,qBAAa,aAAa;IAWtB,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAX1B,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,KAAK,CAAS;IACtB,+FAA+F;IAC/F,OAAO,CAAC,OAAO,CAA6D;IAC5E,wFAAwF;IACxF,OAAO,CAAC,cAAc,CAAK;gBAGR,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,eAAe;IAG3C,sFAAsF;IACtF,KAAK,IAAI,IAAI;IAKb,IAAI,IAAI,IAAI;IAMZ,qEAAqE;IACrE,IAAI,IAAI,IAAI;YAIE,IAAI;YAqCJ,IAAI;IAkClB,OAAO,CAAC,GAAG;CAiBZ"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { isEmailError } from '@fluxmail/core';
|
|
2
|
+
import { claimScheduledSend, completeClaim, failClaim, holdPending, listActive, listClaimable, retryClaim, } from '../storage/scheduledSends.js';
|
|
3
|
+
/** Re-check at least this often while anything is pending (heals clock jumps and laptop sleep). */
|
|
4
|
+
const MAX_ARM_MS = 15 * 60_000;
|
|
5
|
+
const BASE_BACKOFF_MS = 30_000;
|
|
6
|
+
const CLAIM_LEASE_MS = 5 * 60_000;
|
|
7
|
+
/** Errors that no amount of retrying will fix (e.g. the draft is gone). */
|
|
8
|
+
const PERMANENT_CODES = new Set(['not_found', 'invalid_request', 'unsupported_capability']);
|
|
9
|
+
/**
|
|
10
|
+
* Fires scheduled sends when their time comes. Owned by the long-lived
|
|
11
|
+
* processes (serve/stdio); one-shot CLI commands construct it but never
|
|
12
|
+
* start() it. Assumes a single long-lived process per database; there is
|
|
13
|
+
* no cross-process locking.
|
|
14
|
+
*/
|
|
15
|
+
export class SendScheduler {
|
|
16
|
+
db;
|
|
17
|
+
service;
|
|
18
|
+
timer;
|
|
19
|
+
running = false;
|
|
20
|
+
processing = false;
|
|
21
|
+
rerun = false;
|
|
22
|
+
/** Retry gates for transient failures; in-memory only (a restart just retries immediately). */
|
|
23
|
+
backoff = new Map();
|
|
24
|
+
/** While over the plan quota, due sends are held and re-checked no sooner than this. */
|
|
25
|
+
quotaHoldUntil = 0;
|
|
26
|
+
constructor(db, service) {
|
|
27
|
+
this.db = db;
|
|
28
|
+
this.service = service;
|
|
29
|
+
}
|
|
30
|
+
/** Starts the loop; immediately fires anything past due (catch-up after downtime). */
|
|
31
|
+
start() {
|
|
32
|
+
this.running = true;
|
|
33
|
+
void this.tick();
|
|
34
|
+
}
|
|
35
|
+
stop() {
|
|
36
|
+
this.running = false;
|
|
37
|
+
if (this.timer)
|
|
38
|
+
clearTimeout(this.timer);
|
|
39
|
+
this.timer = undefined;
|
|
40
|
+
}
|
|
41
|
+
/** Poke after any schedule change (new, canceled, or sent early). */
|
|
42
|
+
wake() {
|
|
43
|
+
if (this.running)
|
|
44
|
+
void this.tick();
|
|
45
|
+
}
|
|
46
|
+
async tick() {
|
|
47
|
+
if (!this.running)
|
|
48
|
+
return;
|
|
49
|
+
if (this.processing) {
|
|
50
|
+
this.rerun = true;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.processing = true;
|
|
54
|
+
try {
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
let due = listClaimable(this.db, now)
|
|
57
|
+
.filter((r) => r.sendAt <= now && (this.backoff.get(r.id)?.notBefore ?? 0) <= now)
|
|
58
|
+
.sort((a, b) => a.sendAt - b.sendAt);
|
|
59
|
+
if (due.length) {
|
|
60
|
+
try {
|
|
61
|
+
this.service.enforceQuota();
|
|
62
|
+
this.quotaHoldUntil = 0;
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
// Over quota after a license lapse: hold due sends (pending, no
|
|
66
|
+
// attempt consumed) and re-check later; they fire once the license
|
|
67
|
+
// is renewed or usage is trimmed to fit the plan.
|
|
68
|
+
this.quotaHoldUntil = now + MAX_ARM_MS;
|
|
69
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
70
|
+
for (const row of due)
|
|
71
|
+
holdPending(this.db, row.id, message);
|
|
72
|
+
due = [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const row of due)
|
|
76
|
+
await this.fire(row);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
this.processing = false;
|
|
80
|
+
}
|
|
81
|
+
if (this.rerun) {
|
|
82
|
+
this.rerun = false;
|
|
83
|
+
return void this.tick();
|
|
84
|
+
}
|
|
85
|
+
this.arm();
|
|
86
|
+
}
|
|
87
|
+
async fire(row) {
|
|
88
|
+
const claim = claimScheduledSend(this.db, row.id, Date.now(), CLAIM_LEASE_MS);
|
|
89
|
+
if (!claim) {
|
|
90
|
+
this.backoff.delete(row.id);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const result = await this.service.send(row.accountId, { draftId: row.draftId });
|
|
95
|
+
completeClaim(this.db, row.id, claim.token, result);
|
|
96
|
+
this.backoff.delete(row.id);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
100
|
+
if (isEmailError(err) && PERMANENT_CODES.has(err.code)) {
|
|
101
|
+
failClaim(this.db, row.id, claim.token, err.code === 'not_found'
|
|
102
|
+
? 'Draft no longer exists: it was sent or deleted outside Fluxmail'
|
|
103
|
+
: message);
|
|
104
|
+
this.backoff.delete(row.id);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Transient (auth_expired, rate_limited, network): retry forever, late but sent.
|
|
108
|
+
retryClaim(this.db, row.id, claim.token, message);
|
|
109
|
+
const attempt = (this.backoff.get(row.id)?.attempt ?? 0) + 1;
|
|
110
|
+
this.backoff.set(row.id, {
|
|
111
|
+
attempt,
|
|
112
|
+
notBefore: Date.now() + Math.min(BASE_BACKOFF_MS * 2 ** (attempt - 1), MAX_ARM_MS),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
arm() {
|
|
118
|
+
if (this.timer)
|
|
119
|
+
clearTimeout(this.timer);
|
|
120
|
+
this.timer = undefined;
|
|
121
|
+
const pending = listActive(this.db);
|
|
122
|
+
if (!pending.length)
|
|
123
|
+
return;
|
|
124
|
+
const next = Math.max(Math.min(...pending.map((r) => Math.max(r.sendAt, r.claimUntil ?? 0, this.backoff.get(r.id)?.notBefore ?? 0))), this.quotaHoldUntil);
|
|
125
|
+
const delay = Math.min(Math.max(next - Date.now(), 0), MAX_ARM_MS);
|
|
126
|
+
this.timer = setTimeout(() => void this.tick(), delay);
|
|
127
|
+
this.timer.unref();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=sendScheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sendScheduler.js","sourceRoot":"","sources":["../../src/scheduler/sendScheduler.ts"],"names":[],"mappings":"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"}
|