mcp-twake-mail 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/LICENSE +663 -0
- package/README.md +332 -0
- package/build/auth/index.d.ts +3 -0
- package/build/auth/index.js +4 -0
- package/build/auth/index.js.map +1 -0
- package/build/auth/oidc-flow.d.ts +47 -0
- package/build/auth/oidc-flow.js +146 -0
- package/build/auth/oidc-flow.js.map +1 -0
- package/build/auth/token-refresh.d.ts +56 -0
- package/build/auth/token-refresh.js +132 -0
- package/build/auth/token-refresh.js.map +1 -0
- package/build/auth/token-store.d.ts +33 -0
- package/build/auth/token-store.js +63 -0
- package/build/auth/token-store.js.map +1 -0
- package/build/cli/commands/auth.d.ts +5 -0
- package/build/cli/commands/auth.js +49 -0
- package/build/cli/commands/auth.js.map +1 -0
- package/build/cli/commands/check.d.ts +4 -0
- package/build/cli/commands/check.js +125 -0
- package/build/cli/commands/check.js.map +1 -0
- package/build/cli/commands/setup.d.ts +4 -0
- package/build/cli/commands/setup.js +172 -0
- package/build/cli/commands/setup.js.map +1 -0
- package/build/cli/index.d.ts +15 -0
- package/build/cli/index.js +56 -0
- package/build/cli/index.js.map +1 -0
- package/build/cli/prompts/setup-wizard.d.ts +38 -0
- package/build/cli/prompts/setup-wizard.js +121 -0
- package/build/cli/prompts/setup-wizard.js.map +1 -0
- package/build/config/__tests__/logger.test.d.ts +1 -0
- package/build/config/__tests__/logger.test.js +28 -0
- package/build/config/__tests__/logger.test.js.map +1 -0
- package/build/config/__tests__/schema.test.d.ts +1 -0
- package/build/config/__tests__/schema.test.js +101 -0
- package/build/config/__tests__/schema.test.js.map +1 -0
- package/build/config/logger.d.ts +3 -0
- package/build/config/logger.js +9 -0
- package/build/config/logger.js.map +1 -0
- package/build/config/schema.d.ts +28 -0
- package/build/config/schema.js +81 -0
- package/build/config/schema.js.map +1 -0
- package/build/errors.d.ts +34 -0
- package/build/errors.js +154 -0
- package/build/errors.js.map +1 -0
- package/build/errors.test.d.ts +1 -0
- package/build/errors.test.js +234 -0
- package/build/errors.test.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +12 -0
- package/build/index.js.map +1 -0
- package/build/jmap/client.d.ts +96 -0
- package/build/jmap/client.js +267 -0
- package/build/jmap/client.js.map +1 -0
- package/build/mcp/server.d.ts +24 -0
- package/build/mcp/server.js +68 -0
- package/build/mcp/server.js.map +1 -0
- package/build/mcp/tools/attachment.d.ts +30 -0
- package/build/mcp/tools/attachment.js +246 -0
- package/build/mcp/tools/attachment.js.map +1 -0
- package/build/mcp/tools/attachment.test.d.ts +1 -0
- package/build/mcp/tools/attachment.test.js +457 -0
- package/build/mcp/tools/attachment.test.js.map +1 -0
- package/build/mcp/tools/email-operations.d.ts +10 -0
- package/build/mcp/tools/email-operations.js +828 -0
- package/build/mcp/tools/email-operations.js.map +1 -0
- package/build/mcp/tools/email-operations.test.d.ts +1 -0
- package/build/mcp/tools/email-operations.test.js +453 -0
- package/build/mcp/tools/email-operations.test.js.map +1 -0
- package/build/mcp/tools/email-sending.d.ts +10 -0
- package/build/mcp/tools/email-sending.js +682 -0
- package/build/mcp/tools/email-sending.js.map +1 -0
- package/build/mcp/tools/email.d.ts +10 -0
- package/build/mcp/tools/email.js +365 -0
- package/build/mcp/tools/email.js.map +1 -0
- package/build/mcp/tools/email.test.d.ts +1 -0
- package/build/mcp/tools/email.test.js +332 -0
- package/build/mcp/tools/email.test.js.map +1 -0
- package/build/mcp/tools/index.d.ts +14 -0
- package/build/mcp/tools/index.js +29 -0
- package/build/mcp/tools/index.js.map +1 -0
- package/build/mcp/tools/mailbox.d.ts +10 -0
- package/build/mcp/tools/mailbox.js +195 -0
- package/build/mcp/tools/mailbox.js.map +1 -0
- package/build/mcp/tools/mailbox.test.d.ts +1 -0
- package/build/mcp/tools/mailbox.test.js +231 -0
- package/build/mcp/tools/mailbox.test.js.map +1 -0
- package/build/mcp/tools/thread.d.ts +10 -0
- package/build/mcp/tools/thread.js +282 -0
- package/build/mcp/tools/thread.js.map +1 -0
- package/build/mcp/tools/thread.test.d.ts +1 -0
- package/build/mcp/tools/thread.test.js +384 -0
- package/build/mcp/tools/thread.test.js.map +1 -0
- package/build/transformers/__tests__/email.test.d.ts +1 -0
- package/build/transformers/__tests__/email.test.js +438 -0
- package/build/transformers/__tests__/email.test.js.map +1 -0
- package/build/transformers/__tests__/mailbox.test.d.ts +1 -0
- package/build/transformers/__tests__/mailbox.test.js +222 -0
- package/build/transformers/__tests__/mailbox.test.js.map +1 -0
- package/build/transformers/email.d.ts +76 -0
- package/build/transformers/email.js +138 -0
- package/build/transformers/email.js.map +1 -0
- package/build/transformers/index.d.ts +5 -0
- package/build/transformers/index.js +6 -0
- package/build/transformers/index.js.map +1 -0
- package/build/transformers/mailbox.d.ts +43 -0
- package/build/transformers/mailbox.js +70 -0
- package/build/transformers/mailbox.js.map +1 -0
- package/build/types/dto.d.ts +91 -0
- package/build/types/dto.js +9 -0
- package/build/types/dto.js.map +1 -0
- package/build/types/jmap.d.ts +110 -0
- package/build/types/jmap.js +5 -0
- package/build/types/jmap.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email sending MCP tools for composing and sending emails via JMAP.
|
|
3
|
+
* Tools: send_email
|
|
4
|
+
* These tools enable AI assistants to send new emails.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
/**
|
|
8
|
+
* Annotations for send operations (not idempotent - each call sends a new email).
|
|
9
|
+
*/
|
|
10
|
+
const EMAIL_SEND_ANNOTATIONS = {
|
|
11
|
+
readOnlyHint: false,
|
|
12
|
+
destructiveHint: false,
|
|
13
|
+
idempotentHint: false,
|
|
14
|
+
openWorldHint: true,
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* JMAP capabilities required for email submission.
|
|
18
|
+
*/
|
|
19
|
+
const SUBMISSION_USING = [
|
|
20
|
+
'urn:ietf:params:jmap:core',
|
|
21
|
+
'urn:ietf:params:jmap:mail',
|
|
22
|
+
'urn:ietf:params:jmap:submission',
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Register email sending MCP tools with the server.
|
|
26
|
+
* @param server MCP server instance
|
|
27
|
+
* @param jmapClient JMAP client for API calls
|
|
28
|
+
* @param logger Pino logger
|
|
29
|
+
*/
|
|
30
|
+
export function registerEmailSendingTools(server, jmapClient, logger) {
|
|
31
|
+
// send_email - compose and send a new email (EMAIL-01)
|
|
32
|
+
server.registerTool('send_email', {
|
|
33
|
+
title: 'Send Email',
|
|
34
|
+
description: 'Compose and send a new email. Supports plain text and HTML body content.',
|
|
35
|
+
inputSchema: {
|
|
36
|
+
to: z
|
|
37
|
+
.array(z.string().email())
|
|
38
|
+
.min(1)
|
|
39
|
+
.describe('Recipient email addresses (at least one required)'),
|
|
40
|
+
cc: z.array(z.string().email()).optional().describe('CC email addresses'),
|
|
41
|
+
bcc: z.array(z.string().email()).optional().describe('BCC email addresses'),
|
|
42
|
+
subject: z.string().describe('Email subject line'),
|
|
43
|
+
body: z.string().optional().describe('Plain text email body'),
|
|
44
|
+
htmlBody: z.string().optional().describe('HTML email body'),
|
|
45
|
+
},
|
|
46
|
+
annotations: EMAIL_SEND_ANNOTATIONS,
|
|
47
|
+
}, async ({ to, cc, bcc, subject, body, htmlBody }) => {
|
|
48
|
+
logger.debug({ to, cc, bcc, subject, hasBody: !!body, hasHtmlBody: !!htmlBody }, 'send_email called');
|
|
49
|
+
try {
|
|
50
|
+
const session = jmapClient.getSession();
|
|
51
|
+
// Step 1: Get identity and mailboxes in a single batch
|
|
52
|
+
// Use Mailbox/get instead of Mailbox/query because some servers don't support filter by role
|
|
53
|
+
const setupResponse = await jmapClient.request([
|
|
54
|
+
['Identity/get', { accountId: session.accountId }, 'getIdentity'],
|
|
55
|
+
[
|
|
56
|
+
'Mailbox/get',
|
|
57
|
+
{ accountId: session.accountId, properties: ['id', 'role'] },
|
|
58
|
+
'getMailboxes',
|
|
59
|
+
],
|
|
60
|
+
], SUBMISSION_USING);
|
|
61
|
+
// Parse Identity response
|
|
62
|
+
const identityResult = jmapClient.parseMethodResponse(setupResponse.methodResponses[0]);
|
|
63
|
+
if (!identityResult.success) {
|
|
64
|
+
logger.error({ error: identityResult.error }, 'Failed to get identity');
|
|
65
|
+
return {
|
|
66
|
+
isError: true,
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: 'text',
|
|
70
|
+
text: 'Failed to get sending identity. Contact your administrator.',
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const identities = identityResult.data.list;
|
|
76
|
+
if (!identities || identities.length === 0) {
|
|
77
|
+
logger.error({}, 'No sending identity available');
|
|
78
|
+
return {
|
|
79
|
+
isError: true,
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: 'text',
|
|
83
|
+
text: 'No sending identity available. Contact your administrator.',
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const identity = identities[0];
|
|
89
|
+
// Parse Mailbox response and find Sent/Drafts by role
|
|
90
|
+
const mailboxResult = jmapClient.parseMethodResponse(setupResponse.methodResponses[1]);
|
|
91
|
+
if (!mailboxResult.success) {
|
|
92
|
+
logger.error({ error: mailboxResult.error }, 'Failed to get mailboxes');
|
|
93
|
+
return {
|
|
94
|
+
isError: true,
|
|
95
|
+
content: [
|
|
96
|
+
{
|
|
97
|
+
type: 'text',
|
|
98
|
+
text: 'Failed to get mailboxes. Cannot send email.',
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const mailboxes = mailboxResult.data.list;
|
|
104
|
+
const sentMailbox = mailboxes.find((mb) => mb.role === 'sent');
|
|
105
|
+
const draftsMailbox = mailboxes.find((mb) => mb.role === 'drafts');
|
|
106
|
+
const sentMailboxId = sentMailbox?.id;
|
|
107
|
+
if (!draftsMailbox) {
|
|
108
|
+
logger.error({}, 'No Drafts mailbox found');
|
|
109
|
+
return {
|
|
110
|
+
isError: true,
|
|
111
|
+
content: [
|
|
112
|
+
{
|
|
113
|
+
type: 'text',
|
|
114
|
+
text: 'No Drafts mailbox found. Cannot send email.',
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const draftsMailboxId = draftsMailbox.id;
|
|
120
|
+
// Step 2: Build textBody/htmlBody for better server compatibility
|
|
121
|
+
// Using textBody/htmlBody instead of bodyStructure as it's better supported
|
|
122
|
+
let textBody;
|
|
123
|
+
let htmlBodyParts;
|
|
124
|
+
const bodyValues = {};
|
|
125
|
+
if (body) {
|
|
126
|
+
textBody = [{ partId: 'text', type: 'text/plain' }];
|
|
127
|
+
bodyValues.text = { value: body };
|
|
128
|
+
}
|
|
129
|
+
if (htmlBody) {
|
|
130
|
+
htmlBodyParts = [{ partId: 'html', type: 'text/html' }];
|
|
131
|
+
bodyValues.html = { value: htmlBody };
|
|
132
|
+
}
|
|
133
|
+
// Step 3: Build email object
|
|
134
|
+
const emailCreate = {
|
|
135
|
+
mailboxIds: { [draftsMailboxId]: true },
|
|
136
|
+
from: [{ name: identity.name, email: identity.email }],
|
|
137
|
+
to: to.map((email) => ({ email })),
|
|
138
|
+
subject,
|
|
139
|
+
bodyValues,
|
|
140
|
+
};
|
|
141
|
+
// Add body parts (server will create multipart/alternative if both present)
|
|
142
|
+
if (textBody) {
|
|
143
|
+
emailCreate.textBody = textBody;
|
|
144
|
+
}
|
|
145
|
+
if (htmlBodyParts) {
|
|
146
|
+
emailCreate.htmlBody = htmlBodyParts;
|
|
147
|
+
}
|
|
148
|
+
// Add optional address fields
|
|
149
|
+
if (cc && cc.length > 0) {
|
|
150
|
+
emailCreate.cc = cc.map((email) => ({ email }));
|
|
151
|
+
}
|
|
152
|
+
if (bcc && bcc.length > 0) {
|
|
153
|
+
emailCreate.bcc = bcc.map((email) => ({ email }));
|
|
154
|
+
}
|
|
155
|
+
// Step 4: Build onSuccessUpdateEmail for Drafts-to-Sent transition
|
|
156
|
+
const onSuccessUpdate = {
|
|
157
|
+
'keywords/$draft': null,
|
|
158
|
+
};
|
|
159
|
+
if (sentMailboxId) {
|
|
160
|
+
onSuccessUpdate[`mailboxIds/${draftsMailboxId}`] = null;
|
|
161
|
+
onSuccessUpdate[`mailboxIds/${sentMailboxId}`] = true;
|
|
162
|
+
}
|
|
163
|
+
// Step 5: Create email and submit in single batch
|
|
164
|
+
const sendResponse = await jmapClient.request([
|
|
165
|
+
[
|
|
166
|
+
'Email/set',
|
|
167
|
+
{
|
|
168
|
+
accountId: session.accountId,
|
|
169
|
+
create: { email: emailCreate },
|
|
170
|
+
},
|
|
171
|
+
'createEmail',
|
|
172
|
+
],
|
|
173
|
+
[
|
|
174
|
+
'EmailSubmission/set',
|
|
175
|
+
{
|
|
176
|
+
accountId: session.accountId,
|
|
177
|
+
create: {
|
|
178
|
+
submission: {
|
|
179
|
+
identityId: identity.id,
|
|
180
|
+
emailId: '#email',
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
onSuccessUpdateEmail: { '#submission': onSuccessUpdate },
|
|
184
|
+
},
|
|
185
|
+
'submitEmail',
|
|
186
|
+
],
|
|
187
|
+
], SUBMISSION_USING);
|
|
188
|
+
// Step 6: Check Email/set response
|
|
189
|
+
const emailResult = jmapClient.parseMethodResponse(sendResponse.methodResponses[0]);
|
|
190
|
+
if (!emailResult.success) {
|
|
191
|
+
logger.error({ error: emailResult.error }, 'JMAP error in Email/set');
|
|
192
|
+
return {
|
|
193
|
+
isError: true,
|
|
194
|
+
content: [
|
|
195
|
+
{
|
|
196
|
+
type: 'text',
|
|
197
|
+
text: `Failed to create email: ${emailResult.error?.description || emailResult.error?.type || 'Unknown error'}`,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const emailSetResponse = emailResult.data;
|
|
203
|
+
if (emailSetResponse.notCreated?.email) {
|
|
204
|
+
const error = emailSetResponse.notCreated.email;
|
|
205
|
+
logger.error({ error }, 'Email/set notCreated');
|
|
206
|
+
return {
|
|
207
|
+
isError: true,
|
|
208
|
+
content: [
|
|
209
|
+
{
|
|
210
|
+
type: 'text',
|
|
211
|
+
text: `Failed to create email: ${error.type} - ${error.description || ''}`,
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const createdEmail = emailSetResponse.created?.email;
|
|
217
|
+
if (!createdEmail) {
|
|
218
|
+
logger.error({}, 'No created email in response');
|
|
219
|
+
return {
|
|
220
|
+
isError: true,
|
|
221
|
+
content: [
|
|
222
|
+
{
|
|
223
|
+
type: 'text',
|
|
224
|
+
text: 'Failed to create email: no created email in response',
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
// Step 7: Check EmailSubmission/set response
|
|
230
|
+
const submissionResult = jmapClient.parseMethodResponse(sendResponse.methodResponses[1]);
|
|
231
|
+
if (!submissionResult.success) {
|
|
232
|
+
logger.error({ error: submissionResult.error }, 'JMAP error in EmailSubmission/set');
|
|
233
|
+
return {
|
|
234
|
+
isError: true,
|
|
235
|
+
content: [
|
|
236
|
+
{
|
|
237
|
+
type: 'text',
|
|
238
|
+
text: `Failed to send email: ${submissionResult.error?.description || submissionResult.error?.type || 'Unknown error'}`,
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const submissionSetResponse = submissionResult.data;
|
|
244
|
+
if (submissionSetResponse.notCreated?.submission) {
|
|
245
|
+
const error = submissionSetResponse.notCreated.submission;
|
|
246
|
+
logger.error({ error }, 'EmailSubmission/set notCreated');
|
|
247
|
+
// Provide user-friendly messages for common errors
|
|
248
|
+
let errorMessage = `Failed to send email: ${error.type}`;
|
|
249
|
+
if (error.type === 'forbiddenFrom') {
|
|
250
|
+
errorMessage = 'Failed to send email: You are not authorized to send from this address.';
|
|
251
|
+
}
|
|
252
|
+
else if (error.type === 'forbiddenToSend') {
|
|
253
|
+
errorMessage = 'Failed to send email: You do not have permission to send emails.';
|
|
254
|
+
}
|
|
255
|
+
else if (error.type === 'tooManyRecipients') {
|
|
256
|
+
errorMessage = 'Failed to send email: Too many recipients specified.';
|
|
257
|
+
}
|
|
258
|
+
else if (error.description) {
|
|
259
|
+
errorMessage = `Failed to send email: ${error.type} - ${error.description}`;
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
isError: true,
|
|
263
|
+
content: [
|
|
264
|
+
{
|
|
265
|
+
type: 'text',
|
|
266
|
+
text: errorMessage,
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const createdSubmission = submissionSetResponse.created?.submission;
|
|
272
|
+
if (!createdSubmission) {
|
|
273
|
+
logger.error({}, 'No created submission in response');
|
|
274
|
+
return {
|
|
275
|
+
isError: true,
|
|
276
|
+
content: [
|
|
277
|
+
{
|
|
278
|
+
type: 'text',
|
|
279
|
+
text: 'Failed to send email: no submission created in response',
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
logger.debug({ emailId: createdEmail.id, submissionId: createdSubmission.id }, 'send_email success');
|
|
285
|
+
return {
|
|
286
|
+
content: [
|
|
287
|
+
{
|
|
288
|
+
type: 'text',
|
|
289
|
+
text: JSON.stringify({
|
|
290
|
+
success: true,
|
|
291
|
+
emailId: createdEmail.id,
|
|
292
|
+
submissionId: createdSubmission.id,
|
|
293
|
+
}),
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
logger.error({ error }, 'Exception in send_email');
|
|
300
|
+
return {
|
|
301
|
+
isError: true,
|
|
302
|
+
content: [
|
|
303
|
+
{
|
|
304
|
+
type: 'text',
|
|
305
|
+
text: `Error sending email: ${error instanceof Error ? error.message : String(error)}`,
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
// reply_email - reply to an existing email with proper threading (EMAIL-02)
|
|
312
|
+
server.registerTool('reply_email', {
|
|
313
|
+
title: 'Reply to Email',
|
|
314
|
+
description: 'Reply to an existing email with proper threading (In-Reply-To, References headers). Supports reply-all to include all original recipients.',
|
|
315
|
+
inputSchema: {
|
|
316
|
+
originalEmailId: z.string().describe('ID of the email being replied to'),
|
|
317
|
+
body: z.string().describe('Plain text reply body'),
|
|
318
|
+
htmlBody: z.string().optional().describe('HTML reply body'),
|
|
319
|
+
replyAll: z
|
|
320
|
+
.boolean()
|
|
321
|
+
.default(false)
|
|
322
|
+
.describe('If true, reply to all original recipients'),
|
|
323
|
+
},
|
|
324
|
+
annotations: EMAIL_SEND_ANNOTATIONS,
|
|
325
|
+
}, async ({ originalEmailId, body, htmlBody, replyAll }) => {
|
|
326
|
+
logger.debug({ originalEmailId, hasBody: !!body, hasHtmlBody: !!htmlBody, replyAll }, 'reply_email called');
|
|
327
|
+
try {
|
|
328
|
+
const session = jmapClient.getSession();
|
|
329
|
+
// Step 1: Get identity, mailboxes, and original email in a single batch
|
|
330
|
+
// Use Mailbox/get instead of Mailbox/query because some servers don't support filter by role
|
|
331
|
+
const setupResponse = await jmapClient.request([
|
|
332
|
+
['Identity/get', { accountId: session.accountId }, 'getIdentity'],
|
|
333
|
+
[
|
|
334
|
+
'Mailbox/get',
|
|
335
|
+
{ accountId: session.accountId, properties: ['id', 'role'] },
|
|
336
|
+
'getMailboxes',
|
|
337
|
+
],
|
|
338
|
+
[
|
|
339
|
+
'Email/get',
|
|
340
|
+
{
|
|
341
|
+
accountId: session.accountId,
|
|
342
|
+
ids: [originalEmailId],
|
|
343
|
+
properties: ['messageId', 'references', 'subject', 'from', 'to', 'cc', 'replyTo'],
|
|
344
|
+
},
|
|
345
|
+
'getOriginal',
|
|
346
|
+
],
|
|
347
|
+
], SUBMISSION_USING);
|
|
348
|
+
// Parse Identity response
|
|
349
|
+
const identityResult = jmapClient.parseMethodResponse(setupResponse.methodResponses[0]);
|
|
350
|
+
if (!identityResult.success) {
|
|
351
|
+
logger.error({ error: identityResult.error }, 'Failed to get identity');
|
|
352
|
+
return {
|
|
353
|
+
isError: true,
|
|
354
|
+
content: [
|
|
355
|
+
{
|
|
356
|
+
type: 'text',
|
|
357
|
+
text: 'Failed to get sending identity. Contact your administrator.',
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const identities = identityResult.data.list;
|
|
363
|
+
if (!identities || identities.length === 0) {
|
|
364
|
+
logger.error({}, 'No sending identity available');
|
|
365
|
+
return {
|
|
366
|
+
isError: true,
|
|
367
|
+
content: [
|
|
368
|
+
{
|
|
369
|
+
type: 'text',
|
|
370
|
+
text: 'No sending identity available. Contact your administrator.',
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
const identity = identities[0];
|
|
376
|
+
// Parse Mailbox response and find Sent/Drafts by role
|
|
377
|
+
const mailboxResult = jmapClient.parseMethodResponse(setupResponse.methodResponses[1]);
|
|
378
|
+
if (!mailboxResult.success) {
|
|
379
|
+
logger.error({ error: mailboxResult.error }, 'Failed to get mailboxes');
|
|
380
|
+
return {
|
|
381
|
+
isError: true,
|
|
382
|
+
content: [
|
|
383
|
+
{
|
|
384
|
+
type: 'text',
|
|
385
|
+
text: 'Failed to get mailboxes. Cannot send reply.',
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const mailboxes = mailboxResult.data.list;
|
|
391
|
+
const sentMailbox = mailboxes.find((mb) => mb.role === 'sent');
|
|
392
|
+
const draftsMailbox = mailboxes.find((mb) => mb.role === 'drafts');
|
|
393
|
+
const sentMailboxId = sentMailbox?.id;
|
|
394
|
+
if (!draftsMailbox) {
|
|
395
|
+
logger.error({}, 'No Drafts mailbox found');
|
|
396
|
+
return {
|
|
397
|
+
isError: true,
|
|
398
|
+
content: [
|
|
399
|
+
{
|
|
400
|
+
type: 'text',
|
|
401
|
+
text: 'No Drafts mailbox found. Cannot send reply.',
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
const draftsMailboxId = draftsMailbox.id;
|
|
407
|
+
// Parse original email response
|
|
408
|
+
const originalResult = jmapClient.parseMethodResponse(setupResponse.methodResponses[2]);
|
|
409
|
+
if (!originalResult.success) {
|
|
410
|
+
logger.error({ error: originalResult.error }, 'Failed to get original email');
|
|
411
|
+
return {
|
|
412
|
+
isError: true,
|
|
413
|
+
content: [
|
|
414
|
+
{
|
|
415
|
+
type: 'text',
|
|
416
|
+
text: `Failed to fetch original email: ${originalResult.error?.description || originalResult.error?.type || 'Unknown error'}`,
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
const originalList = originalResult.data.list;
|
|
422
|
+
if (!originalList || originalList.length === 0) {
|
|
423
|
+
logger.error({ originalEmailId }, 'Original email not found');
|
|
424
|
+
return {
|
|
425
|
+
isError: true,
|
|
426
|
+
content: [
|
|
427
|
+
{
|
|
428
|
+
type: 'text',
|
|
429
|
+
text: `Original email not found: ${originalEmailId}`,
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
const original = originalList[0];
|
|
435
|
+
// Step 2: Build threading headers (arrays per RFC 8621)
|
|
436
|
+
const inReplyTo = original.messageId || [];
|
|
437
|
+
const references = [
|
|
438
|
+
...(original.references || []),
|
|
439
|
+
...(original.messageId || []),
|
|
440
|
+
];
|
|
441
|
+
// Step 3: Build subject with Re: prefix
|
|
442
|
+
let replySubject;
|
|
443
|
+
const originalSubject = original.subject || '';
|
|
444
|
+
if (originalSubject.toLowerCase().startsWith('re:')) {
|
|
445
|
+
replySubject = originalSubject;
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
replySubject = `Re: ${originalSubject}`;
|
|
449
|
+
}
|
|
450
|
+
// Step 4: Build recipients
|
|
451
|
+
// Primary recipient: replyTo if available, otherwise from
|
|
452
|
+
const primaryRecipient = original.replyTo?.[0] || original.from?.[0];
|
|
453
|
+
if (!primaryRecipient) {
|
|
454
|
+
logger.error({}, 'No recipient found in original email');
|
|
455
|
+
return {
|
|
456
|
+
isError: true,
|
|
457
|
+
content: [
|
|
458
|
+
{
|
|
459
|
+
type: 'text',
|
|
460
|
+
text: 'Original email has no sender address to reply to.',
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
const toAddresses = [primaryRecipient];
|
|
466
|
+
const ccAddresses = [];
|
|
467
|
+
if (replyAll) {
|
|
468
|
+
// Add all original 'to' recipients except self (case-insensitive)
|
|
469
|
+
const selfEmail = identity.email.toLowerCase();
|
|
470
|
+
for (const addr of original.to || []) {
|
|
471
|
+
if (addr.email.toLowerCase() !== selfEmail) {
|
|
472
|
+
// Avoid duplicates with primary recipient
|
|
473
|
+
if (addr.email.toLowerCase() !== primaryRecipient.email.toLowerCase()) {
|
|
474
|
+
toAddresses.push(addr);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// Add all original 'cc' recipients except self
|
|
479
|
+
for (const addr of original.cc || []) {
|
|
480
|
+
if (addr.email.toLowerCase() !== selfEmail) {
|
|
481
|
+
ccAddresses.push(addr);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// Step 5: Build textBody/htmlBody for better server compatibility
|
|
486
|
+
let textBody;
|
|
487
|
+
let htmlBodyParts;
|
|
488
|
+
const bodyValues = {};
|
|
489
|
+
if (body) {
|
|
490
|
+
textBody = [{ partId: 'text', type: 'text/plain' }];
|
|
491
|
+
bodyValues.text = { value: body };
|
|
492
|
+
}
|
|
493
|
+
if (htmlBody) {
|
|
494
|
+
htmlBodyParts = [{ partId: 'html', type: 'text/html' }];
|
|
495
|
+
bodyValues.html = { value: htmlBody };
|
|
496
|
+
}
|
|
497
|
+
// Step 6: Build email object
|
|
498
|
+
const emailCreate = {
|
|
499
|
+
mailboxIds: { [draftsMailboxId]: true },
|
|
500
|
+
from: [{ name: identity.name, email: identity.email }],
|
|
501
|
+
to: toAddresses,
|
|
502
|
+
subject: replySubject,
|
|
503
|
+
inReplyTo,
|
|
504
|
+
references,
|
|
505
|
+
bodyValues,
|
|
506
|
+
};
|
|
507
|
+
// Add body parts (server will create multipart/alternative if both present)
|
|
508
|
+
if (textBody) {
|
|
509
|
+
emailCreate.textBody = textBody;
|
|
510
|
+
}
|
|
511
|
+
if (htmlBodyParts) {
|
|
512
|
+
emailCreate.htmlBody = htmlBodyParts;
|
|
513
|
+
}
|
|
514
|
+
// Add cc if non-empty
|
|
515
|
+
if (ccAddresses.length > 0) {
|
|
516
|
+
emailCreate.cc = ccAddresses;
|
|
517
|
+
}
|
|
518
|
+
// Step 7: Build onSuccessUpdateEmail for Drafts-to-Sent transition
|
|
519
|
+
const onSuccessUpdate = {
|
|
520
|
+
'keywords/$draft': null,
|
|
521
|
+
};
|
|
522
|
+
if (sentMailboxId) {
|
|
523
|
+
onSuccessUpdate[`mailboxIds/${draftsMailboxId}`] = null;
|
|
524
|
+
onSuccessUpdate[`mailboxIds/${sentMailboxId}`] = true;
|
|
525
|
+
}
|
|
526
|
+
// Step 8: Create email and submit in single batch
|
|
527
|
+
const sendResponse = await jmapClient.request([
|
|
528
|
+
[
|
|
529
|
+
'Email/set',
|
|
530
|
+
{
|
|
531
|
+
accountId: session.accountId,
|
|
532
|
+
create: { reply: emailCreate },
|
|
533
|
+
},
|
|
534
|
+
'createReply',
|
|
535
|
+
],
|
|
536
|
+
[
|
|
537
|
+
'EmailSubmission/set',
|
|
538
|
+
{
|
|
539
|
+
accountId: session.accountId,
|
|
540
|
+
create: {
|
|
541
|
+
submission: {
|
|
542
|
+
identityId: identity.id,
|
|
543
|
+
emailId: '#reply',
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
onSuccessUpdateEmail: { '#submission': onSuccessUpdate },
|
|
547
|
+
},
|
|
548
|
+
'submitReply',
|
|
549
|
+
],
|
|
550
|
+
], SUBMISSION_USING);
|
|
551
|
+
// Step 9: Check Email/set response
|
|
552
|
+
const emailResult = jmapClient.parseMethodResponse(sendResponse.methodResponses[0]);
|
|
553
|
+
if (!emailResult.success) {
|
|
554
|
+
logger.error({ error: emailResult.error }, 'JMAP error in Email/set for reply');
|
|
555
|
+
return {
|
|
556
|
+
isError: true,
|
|
557
|
+
content: [
|
|
558
|
+
{
|
|
559
|
+
type: 'text',
|
|
560
|
+
text: `Failed to create reply: ${emailResult.error?.description || emailResult.error?.type || 'Unknown error'}`,
|
|
561
|
+
},
|
|
562
|
+
],
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
const emailSetResponse = emailResult.data;
|
|
566
|
+
if (emailSetResponse.notCreated?.reply) {
|
|
567
|
+
const error = emailSetResponse.notCreated.reply;
|
|
568
|
+
logger.error({ error }, 'Email/set notCreated for reply');
|
|
569
|
+
return {
|
|
570
|
+
isError: true,
|
|
571
|
+
content: [
|
|
572
|
+
{
|
|
573
|
+
type: 'text',
|
|
574
|
+
text: `Failed to create reply: ${error.type} - ${error.description || ''}`,
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
const createdEmail = emailSetResponse.created?.reply;
|
|
580
|
+
if (!createdEmail) {
|
|
581
|
+
logger.error({}, 'No created reply in response');
|
|
582
|
+
return {
|
|
583
|
+
isError: true,
|
|
584
|
+
content: [
|
|
585
|
+
{
|
|
586
|
+
type: 'text',
|
|
587
|
+
text: 'Failed to create reply: no created email in response',
|
|
588
|
+
},
|
|
589
|
+
],
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
// Step 10: Check EmailSubmission/set response
|
|
593
|
+
const submissionResult = jmapClient.parseMethodResponse(sendResponse.methodResponses[1]);
|
|
594
|
+
if (!submissionResult.success) {
|
|
595
|
+
logger.error({ error: submissionResult.error }, 'JMAP error in EmailSubmission/set for reply');
|
|
596
|
+
return {
|
|
597
|
+
isError: true,
|
|
598
|
+
content: [
|
|
599
|
+
{
|
|
600
|
+
type: 'text',
|
|
601
|
+
text: `Failed to send reply: ${submissionResult.error?.description || submissionResult.error?.type || 'Unknown error'}`,
|
|
602
|
+
},
|
|
603
|
+
],
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
const submissionSetResponse = submissionResult.data;
|
|
607
|
+
if (submissionSetResponse.notCreated?.submission) {
|
|
608
|
+
const error = submissionSetResponse.notCreated.submission;
|
|
609
|
+
logger.error({ error }, 'EmailSubmission/set notCreated for reply');
|
|
610
|
+
let errorMessage = `Failed to send reply: ${error.type}`;
|
|
611
|
+
if (error.type === 'forbiddenFrom') {
|
|
612
|
+
errorMessage = 'Failed to send reply: You are not authorized to send from this address.';
|
|
613
|
+
}
|
|
614
|
+
else if (error.type === 'forbiddenToSend') {
|
|
615
|
+
errorMessage = 'Failed to send reply: You do not have permission to send emails.';
|
|
616
|
+
}
|
|
617
|
+
else if (error.type === 'tooManyRecipients') {
|
|
618
|
+
errorMessage = 'Failed to send reply: Too many recipients specified.';
|
|
619
|
+
}
|
|
620
|
+
else if (error.description) {
|
|
621
|
+
errorMessage = `Failed to send reply: ${error.type} - ${error.description}`;
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
isError: true,
|
|
625
|
+
content: [
|
|
626
|
+
{
|
|
627
|
+
type: 'text',
|
|
628
|
+
text: errorMessage,
|
|
629
|
+
},
|
|
630
|
+
],
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
const createdSubmission = submissionSetResponse.created?.submission;
|
|
634
|
+
if (!createdSubmission) {
|
|
635
|
+
logger.error({}, 'No created submission in response for reply');
|
|
636
|
+
return {
|
|
637
|
+
isError: true,
|
|
638
|
+
content: [
|
|
639
|
+
{
|
|
640
|
+
type: 'text',
|
|
641
|
+
text: 'Failed to send reply: no submission created in response',
|
|
642
|
+
},
|
|
643
|
+
],
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
logger.debug({
|
|
647
|
+
emailId: createdEmail.id,
|
|
648
|
+
submissionId: createdSubmission.id,
|
|
649
|
+
threadId: createdEmail.threadId,
|
|
650
|
+
inReplyTo,
|
|
651
|
+
references,
|
|
652
|
+
}, 'reply_email success');
|
|
653
|
+
return {
|
|
654
|
+
content: [
|
|
655
|
+
{
|
|
656
|
+
type: 'text',
|
|
657
|
+
text: JSON.stringify({
|
|
658
|
+
success: true,
|
|
659
|
+
emailId: createdEmail.id,
|
|
660
|
+
submissionId: createdSubmission.id,
|
|
661
|
+
threadId: createdEmail.threadId,
|
|
662
|
+
}),
|
|
663
|
+
},
|
|
664
|
+
],
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
catch (error) {
|
|
668
|
+
logger.error({ error }, 'Exception in reply_email');
|
|
669
|
+
return {
|
|
670
|
+
isError: true,
|
|
671
|
+
content: [
|
|
672
|
+
{
|
|
673
|
+
type: 'text',
|
|
674
|
+
text: `Error sending reply: ${error instanceof Error ? error.message : String(error)}`,
|
|
675
|
+
},
|
|
676
|
+
],
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
logger.debug('Email sending tools registered: send_email, reply_email');
|
|
681
|
+
}
|
|
682
|
+
//# sourceMappingURL=email-sending.js.map
|