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.
Files changed (114) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +332 -0
  3. package/build/auth/index.d.ts +3 -0
  4. package/build/auth/index.js +4 -0
  5. package/build/auth/index.js.map +1 -0
  6. package/build/auth/oidc-flow.d.ts +47 -0
  7. package/build/auth/oidc-flow.js +146 -0
  8. package/build/auth/oidc-flow.js.map +1 -0
  9. package/build/auth/token-refresh.d.ts +56 -0
  10. package/build/auth/token-refresh.js +132 -0
  11. package/build/auth/token-refresh.js.map +1 -0
  12. package/build/auth/token-store.d.ts +33 -0
  13. package/build/auth/token-store.js +63 -0
  14. package/build/auth/token-store.js.map +1 -0
  15. package/build/cli/commands/auth.d.ts +5 -0
  16. package/build/cli/commands/auth.js +49 -0
  17. package/build/cli/commands/auth.js.map +1 -0
  18. package/build/cli/commands/check.d.ts +4 -0
  19. package/build/cli/commands/check.js +125 -0
  20. package/build/cli/commands/check.js.map +1 -0
  21. package/build/cli/commands/setup.d.ts +4 -0
  22. package/build/cli/commands/setup.js +172 -0
  23. package/build/cli/commands/setup.js.map +1 -0
  24. package/build/cli/index.d.ts +15 -0
  25. package/build/cli/index.js +56 -0
  26. package/build/cli/index.js.map +1 -0
  27. package/build/cli/prompts/setup-wizard.d.ts +38 -0
  28. package/build/cli/prompts/setup-wizard.js +121 -0
  29. package/build/cli/prompts/setup-wizard.js.map +1 -0
  30. package/build/config/__tests__/logger.test.d.ts +1 -0
  31. package/build/config/__tests__/logger.test.js +28 -0
  32. package/build/config/__tests__/logger.test.js.map +1 -0
  33. package/build/config/__tests__/schema.test.d.ts +1 -0
  34. package/build/config/__tests__/schema.test.js +101 -0
  35. package/build/config/__tests__/schema.test.js.map +1 -0
  36. package/build/config/logger.d.ts +3 -0
  37. package/build/config/logger.js +9 -0
  38. package/build/config/logger.js.map +1 -0
  39. package/build/config/schema.d.ts +28 -0
  40. package/build/config/schema.js +81 -0
  41. package/build/config/schema.js.map +1 -0
  42. package/build/errors.d.ts +34 -0
  43. package/build/errors.js +154 -0
  44. package/build/errors.js.map +1 -0
  45. package/build/errors.test.d.ts +1 -0
  46. package/build/errors.test.js +234 -0
  47. package/build/errors.test.js.map +1 -0
  48. package/build/index.d.ts +2 -0
  49. package/build/index.js +12 -0
  50. package/build/index.js.map +1 -0
  51. package/build/jmap/client.d.ts +96 -0
  52. package/build/jmap/client.js +267 -0
  53. package/build/jmap/client.js.map +1 -0
  54. package/build/mcp/server.d.ts +24 -0
  55. package/build/mcp/server.js +68 -0
  56. package/build/mcp/server.js.map +1 -0
  57. package/build/mcp/tools/attachment.d.ts +30 -0
  58. package/build/mcp/tools/attachment.js +246 -0
  59. package/build/mcp/tools/attachment.js.map +1 -0
  60. package/build/mcp/tools/attachment.test.d.ts +1 -0
  61. package/build/mcp/tools/attachment.test.js +457 -0
  62. package/build/mcp/tools/attachment.test.js.map +1 -0
  63. package/build/mcp/tools/email-operations.d.ts +10 -0
  64. package/build/mcp/tools/email-operations.js +828 -0
  65. package/build/mcp/tools/email-operations.js.map +1 -0
  66. package/build/mcp/tools/email-operations.test.d.ts +1 -0
  67. package/build/mcp/tools/email-operations.test.js +453 -0
  68. package/build/mcp/tools/email-operations.test.js.map +1 -0
  69. package/build/mcp/tools/email-sending.d.ts +10 -0
  70. package/build/mcp/tools/email-sending.js +682 -0
  71. package/build/mcp/tools/email-sending.js.map +1 -0
  72. package/build/mcp/tools/email.d.ts +10 -0
  73. package/build/mcp/tools/email.js +365 -0
  74. package/build/mcp/tools/email.js.map +1 -0
  75. package/build/mcp/tools/email.test.d.ts +1 -0
  76. package/build/mcp/tools/email.test.js +332 -0
  77. package/build/mcp/tools/email.test.js.map +1 -0
  78. package/build/mcp/tools/index.d.ts +14 -0
  79. package/build/mcp/tools/index.js +29 -0
  80. package/build/mcp/tools/index.js.map +1 -0
  81. package/build/mcp/tools/mailbox.d.ts +10 -0
  82. package/build/mcp/tools/mailbox.js +195 -0
  83. package/build/mcp/tools/mailbox.js.map +1 -0
  84. package/build/mcp/tools/mailbox.test.d.ts +1 -0
  85. package/build/mcp/tools/mailbox.test.js +231 -0
  86. package/build/mcp/tools/mailbox.test.js.map +1 -0
  87. package/build/mcp/tools/thread.d.ts +10 -0
  88. package/build/mcp/tools/thread.js +282 -0
  89. package/build/mcp/tools/thread.js.map +1 -0
  90. package/build/mcp/tools/thread.test.d.ts +1 -0
  91. package/build/mcp/tools/thread.test.js +384 -0
  92. package/build/mcp/tools/thread.test.js.map +1 -0
  93. package/build/transformers/__tests__/email.test.d.ts +1 -0
  94. package/build/transformers/__tests__/email.test.js +438 -0
  95. package/build/transformers/__tests__/email.test.js.map +1 -0
  96. package/build/transformers/__tests__/mailbox.test.d.ts +1 -0
  97. package/build/transformers/__tests__/mailbox.test.js +222 -0
  98. package/build/transformers/__tests__/mailbox.test.js.map +1 -0
  99. package/build/transformers/email.d.ts +76 -0
  100. package/build/transformers/email.js +138 -0
  101. package/build/transformers/email.js.map +1 -0
  102. package/build/transformers/index.d.ts +5 -0
  103. package/build/transformers/index.js +6 -0
  104. package/build/transformers/index.js.map +1 -0
  105. package/build/transformers/mailbox.d.ts +43 -0
  106. package/build/transformers/mailbox.js +70 -0
  107. package/build/transformers/mailbox.js.map +1 -0
  108. package/build/types/dto.d.ts +91 -0
  109. package/build/types/dto.js +9 -0
  110. package/build/types/dto.js.map +1 -0
  111. package/build/types/jmap.d.ts +110 -0
  112. package/build/types/jmap.js +5 -0
  113. package/build/types/jmap.js.map +1 -0
  114. 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