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,828 @@
1
+ /**
2
+ * Email write operation MCP tools for managing emails via JMAP.
3
+ * Tools: mark_as_read, mark_as_unread, delete_email, move_email, add_label, remove_label, create_draft.
4
+ * These tools enable AI assistants to modify email status, organize emails, and create drafts.
5
+ */
6
+ import { z } from 'zod';
7
+ /**
8
+ * Common annotations for non-destructive write operations.
9
+ */
10
+ const EMAIL_WRITE_ANNOTATIONS = {
11
+ readOnlyHint: false,
12
+ destructiveHint: false,
13
+ idempotentHint: true,
14
+ openWorldHint: true,
15
+ };
16
+ /**
17
+ * Annotations for destructive operations (delete).
18
+ */
19
+ const EMAIL_DESTRUCTIVE_ANNOTATIONS = {
20
+ readOnlyHint: false,
21
+ destructiveHint: true,
22
+ idempotentHint: false,
23
+ openWorldHint: true,
24
+ };
25
+ /**
26
+ * Annotations for create operations (not idempotent - each call creates new item).
27
+ */
28
+ const EMAIL_CREATE_ANNOTATIONS = {
29
+ readOnlyHint: false,
30
+ destructiveHint: false,
31
+ idempotentHint: false,
32
+ openWorldHint: true,
33
+ };
34
+ /**
35
+ * Register email operation MCP tools with the server.
36
+ * @param server MCP server instance
37
+ * @param jmapClient JMAP client for API calls
38
+ * @param logger Pino logger
39
+ */
40
+ export function registerEmailOperationTools(server, jmapClient, logger) {
41
+ // mark_as_read - set $seen keyword (EMAIL-06)
42
+ server.registerTool('mark_as_read', {
43
+ title: 'Mark Email as Read',
44
+ description: 'Mark an email as read by setting the $seen keyword.',
45
+ inputSchema: {
46
+ emailId: z.string().describe('The unique identifier of the email to mark as read'),
47
+ },
48
+ annotations: EMAIL_WRITE_ANNOTATIONS,
49
+ }, async ({ emailId }) => {
50
+ logger.debug({ emailId }, 'mark_as_read called');
51
+ try {
52
+ const session = jmapClient.getSession();
53
+ const response = await jmapClient.request([
54
+ [
55
+ 'Email/set',
56
+ {
57
+ accountId: session.accountId,
58
+ update: {
59
+ [emailId]: {
60
+ 'keywords/$seen': true,
61
+ },
62
+ },
63
+ },
64
+ 'markRead',
65
+ ],
66
+ ]);
67
+ const result = jmapClient.parseMethodResponse(response.methodResponses[0]);
68
+ if (!result.success) {
69
+ logger.error({ error: result.error, emailId }, 'JMAP error in mark_as_read');
70
+ return {
71
+ isError: true,
72
+ content: [
73
+ {
74
+ type: 'text',
75
+ text: `Failed to mark email as read: ${result.error?.description || result.error?.type || 'Unknown error'}`,
76
+ },
77
+ ],
78
+ };
79
+ }
80
+ const setResponse = result.data;
81
+ if (setResponse.notUpdated?.[emailId]) {
82
+ const error = setResponse.notUpdated[emailId];
83
+ logger.error({ error, emailId }, 'Email/set notUpdated in mark_as_read');
84
+ return {
85
+ isError: true,
86
+ content: [
87
+ {
88
+ type: 'text',
89
+ text: `Failed to mark email as read: ${error.type} - ${error.description || ''}`,
90
+ },
91
+ ],
92
+ };
93
+ }
94
+ logger.debug({ emailId }, 'mark_as_read success');
95
+ return {
96
+ content: [
97
+ {
98
+ type: 'text',
99
+ text: JSON.stringify({ success: true, emailId, marked: 'read' }),
100
+ },
101
+ ],
102
+ };
103
+ }
104
+ catch (error) {
105
+ logger.error({ error, emailId }, 'Exception in mark_as_read');
106
+ return {
107
+ isError: true,
108
+ content: [
109
+ {
110
+ type: 'text',
111
+ text: `Error marking email as read: ${error instanceof Error ? error.message : String(error)}`,
112
+ },
113
+ ],
114
+ };
115
+ }
116
+ });
117
+ // mark_as_unread - remove $seen keyword (EMAIL-07)
118
+ server.registerTool('mark_as_unread', {
119
+ title: 'Mark Email as Unread',
120
+ description: 'Mark an email as unread by removing the $seen keyword.',
121
+ inputSchema: {
122
+ emailId: z.string().describe('The unique identifier of the email to mark as unread'),
123
+ },
124
+ annotations: EMAIL_WRITE_ANNOTATIONS,
125
+ }, async ({ emailId }) => {
126
+ logger.debug({ emailId }, 'mark_as_unread called');
127
+ try {
128
+ const session = jmapClient.getSession();
129
+ const response = await jmapClient.request([
130
+ [
131
+ 'Email/set',
132
+ {
133
+ accountId: session.accountId,
134
+ update: {
135
+ [emailId]: {
136
+ 'keywords/$seen': null,
137
+ },
138
+ },
139
+ },
140
+ 'markUnread',
141
+ ],
142
+ ]);
143
+ const result = jmapClient.parseMethodResponse(response.methodResponses[0]);
144
+ if (!result.success) {
145
+ logger.error({ error: result.error, emailId }, 'JMAP error in mark_as_unread');
146
+ return {
147
+ isError: true,
148
+ content: [
149
+ {
150
+ type: 'text',
151
+ text: `Failed to mark email as unread: ${result.error?.description || result.error?.type || 'Unknown error'}`,
152
+ },
153
+ ],
154
+ };
155
+ }
156
+ const setResponse = result.data;
157
+ if (setResponse.notUpdated?.[emailId]) {
158
+ const error = setResponse.notUpdated[emailId];
159
+ logger.error({ error, emailId }, 'Email/set notUpdated in mark_as_unread');
160
+ return {
161
+ isError: true,
162
+ content: [
163
+ {
164
+ type: 'text',
165
+ text: `Failed to mark email as unread: ${error.type} - ${error.description || ''}`,
166
+ },
167
+ ],
168
+ };
169
+ }
170
+ logger.debug({ emailId }, 'mark_as_unread success');
171
+ return {
172
+ content: [
173
+ {
174
+ type: 'text',
175
+ text: JSON.stringify({ success: true, emailId, marked: 'unread' }),
176
+ },
177
+ ],
178
+ };
179
+ }
180
+ catch (error) {
181
+ logger.error({ error, emailId }, 'Exception in mark_as_unread');
182
+ return {
183
+ isError: true,
184
+ content: [
185
+ {
186
+ type: 'text',
187
+ text: `Error marking email as unread: ${error instanceof Error ? error.message : String(error)}`,
188
+ },
189
+ ],
190
+ };
191
+ }
192
+ });
193
+ // delete_email - move to Trash or permanently destroy (EMAIL-05)
194
+ server.registerTool('delete_email', {
195
+ title: 'Delete Email',
196
+ description: 'Delete an email. By default moves to Trash. Use permanent=true to permanently destroy.',
197
+ inputSchema: {
198
+ emailId: z.string().describe('The unique identifier of the email to delete'),
199
+ permanent: z
200
+ .boolean()
201
+ .default(false)
202
+ .describe('If true, permanently destroy the email. Default: false (move to Trash)'),
203
+ },
204
+ annotations: EMAIL_DESTRUCTIVE_ANNOTATIONS,
205
+ }, async ({ emailId, permanent }) => {
206
+ logger.debug({ emailId, permanent }, 'delete_email called');
207
+ try {
208
+ const session = jmapClient.getSession();
209
+ if (permanent) {
210
+ // Permanent delete using destroy
211
+ const response = await jmapClient.request([
212
+ [
213
+ 'Email/set',
214
+ {
215
+ accountId: session.accountId,
216
+ destroy: [emailId],
217
+ },
218
+ 'destroyEmail',
219
+ ],
220
+ ]);
221
+ const result = jmapClient.parseMethodResponse(response.methodResponses[0]);
222
+ if (!result.success) {
223
+ logger.error({ error: result.error, emailId }, 'JMAP error in delete_email (permanent)');
224
+ return {
225
+ isError: true,
226
+ content: [
227
+ {
228
+ type: 'text',
229
+ text: `Failed to delete email: ${result.error?.description || result.error?.type || 'Unknown error'}`,
230
+ },
231
+ ],
232
+ };
233
+ }
234
+ const setResponse = result.data;
235
+ if (setResponse.notDestroyed?.[emailId]) {
236
+ const error = setResponse.notDestroyed[emailId];
237
+ logger.error({ error, emailId }, 'Email/set notDestroyed in delete_email');
238
+ return {
239
+ isError: true,
240
+ content: [
241
+ {
242
+ type: 'text',
243
+ text: `Failed to delete email: ${error.type} - ${error.description || ''}`,
244
+ },
245
+ ],
246
+ };
247
+ }
248
+ logger.debug({ emailId }, 'delete_email (permanent) success');
249
+ return {
250
+ content: [
251
+ {
252
+ type: 'text',
253
+ text: JSON.stringify({ success: true, emailId, action: 'permanently_deleted' }),
254
+ },
255
+ ],
256
+ };
257
+ }
258
+ else {
259
+ // Move to Trash
260
+ // First find Trash mailbox
261
+ const mailboxResponse = await jmapClient.request([
262
+ [
263
+ 'Mailbox/query',
264
+ {
265
+ accountId: session.accountId,
266
+ filter: { role: 'trash' },
267
+ },
268
+ 'findTrash',
269
+ ],
270
+ ]);
271
+ const queryResult = jmapClient.parseMethodResponse(mailboxResponse.methodResponses[0]);
272
+ if (!queryResult.success) {
273
+ logger.error({ error: queryResult.error }, 'JMAP error finding Trash mailbox');
274
+ return {
275
+ isError: true,
276
+ content: [
277
+ {
278
+ type: 'text',
279
+ text: 'Failed to find Trash mailbox',
280
+ },
281
+ ],
282
+ };
283
+ }
284
+ const trashIds = queryResult.data.ids;
285
+ if (!trashIds || trashIds.length === 0) {
286
+ // No Trash mailbox, fall back to permanent delete
287
+ logger.warn({ emailId }, 'No Trash mailbox found, performing permanent delete');
288
+ const destroyResponse = await jmapClient.request([
289
+ [
290
+ 'Email/set',
291
+ {
292
+ accountId: session.accountId,
293
+ destroy: [emailId],
294
+ },
295
+ 'destroyEmailFallback',
296
+ ],
297
+ ]);
298
+ const destroyResult = jmapClient.parseMethodResponse(destroyResponse.methodResponses[0]);
299
+ if (!destroyResult.success) {
300
+ logger.error({ error: destroyResult.error, emailId }, 'JMAP error in delete_email (fallback)');
301
+ return {
302
+ isError: true,
303
+ content: [
304
+ {
305
+ type: 'text',
306
+ text: `Failed to delete email: ${destroyResult.error?.description || destroyResult.error?.type || 'Unknown error'}`,
307
+ },
308
+ ],
309
+ };
310
+ }
311
+ const destroySetResponse = destroyResult.data;
312
+ if (destroySetResponse.notDestroyed?.[emailId]) {
313
+ const error = destroySetResponse.notDestroyed[emailId];
314
+ logger.error({ error, emailId }, 'Email/set notDestroyed in delete_email (fallback)');
315
+ return {
316
+ isError: true,
317
+ content: [
318
+ {
319
+ type: 'text',
320
+ text: `Failed to delete email: ${error.type} - ${error.description || ''}`,
321
+ },
322
+ ],
323
+ };
324
+ }
325
+ return {
326
+ content: [
327
+ {
328
+ type: 'text',
329
+ text: JSON.stringify({ success: true, emailId, action: 'permanently_deleted' }),
330
+ },
331
+ ],
332
+ };
333
+ }
334
+ const trashMailboxId = trashIds[0];
335
+ // Move to Trash (replace all mailboxIds with just Trash)
336
+ const moveResponse = await jmapClient.request([
337
+ [
338
+ 'Email/set',
339
+ {
340
+ accountId: session.accountId,
341
+ update: {
342
+ [emailId]: {
343
+ mailboxIds: { [trashMailboxId]: true },
344
+ },
345
+ },
346
+ },
347
+ 'moveToTrash',
348
+ ],
349
+ ]);
350
+ const moveResult = jmapClient.parseMethodResponse(moveResponse.methodResponses[0]);
351
+ if (!moveResult.success) {
352
+ logger.error({ error: moveResult.error, emailId }, 'JMAP error in delete_email (move)');
353
+ return {
354
+ isError: true,
355
+ content: [
356
+ {
357
+ type: 'text',
358
+ text: `Failed to move email to Trash: ${moveResult.error?.description || moveResult.error?.type || 'Unknown error'}`,
359
+ },
360
+ ],
361
+ };
362
+ }
363
+ const moveSetResponse = moveResult.data;
364
+ if (moveSetResponse.notUpdated?.[emailId]) {
365
+ const error = moveSetResponse.notUpdated[emailId];
366
+ logger.error({ error, emailId }, 'Email/set notUpdated in delete_email (move)');
367
+ return {
368
+ isError: true,
369
+ content: [
370
+ {
371
+ type: 'text',
372
+ text: `Failed to move email to Trash: ${error.type} - ${error.description || ''}`,
373
+ },
374
+ ],
375
+ };
376
+ }
377
+ logger.debug({ emailId, trashMailboxId }, 'delete_email (move to Trash) success');
378
+ return {
379
+ content: [
380
+ {
381
+ type: 'text',
382
+ text: JSON.stringify({ success: true, emailId, action: 'moved_to_trash' }),
383
+ },
384
+ ],
385
+ };
386
+ }
387
+ }
388
+ catch (error) {
389
+ logger.error({ error, emailId }, 'Exception in delete_email');
390
+ return {
391
+ isError: true,
392
+ content: [
393
+ {
394
+ type: 'text',
395
+ text: `Error deleting email: ${error instanceof Error ? error.message : String(error)}`,
396
+ },
397
+ ],
398
+ };
399
+ }
400
+ });
401
+ // move_email - move email to a different mailbox (EMAIL-08)
402
+ server.registerTool('move_email', {
403
+ title: 'Move Email',
404
+ description: 'Move an email to a different mailbox. This replaces all current mailbox associations.',
405
+ inputSchema: {
406
+ emailId: z.string().describe('The unique identifier of the email to move'),
407
+ targetMailboxId: z.string().describe('The ID of the mailbox to move the email to'),
408
+ },
409
+ annotations: EMAIL_WRITE_ANNOTATIONS,
410
+ }, async ({ emailId, targetMailboxId }) => {
411
+ logger.debug({ emailId, targetMailboxId }, 'move_email called');
412
+ try {
413
+ const session = jmapClient.getSession();
414
+ const response = await jmapClient.request([
415
+ [
416
+ 'Email/set',
417
+ {
418
+ accountId: session.accountId,
419
+ update: {
420
+ [emailId]: {
421
+ mailboxIds: { [targetMailboxId]: true },
422
+ },
423
+ },
424
+ },
425
+ 'moveEmail',
426
+ ],
427
+ ]);
428
+ const result = jmapClient.parseMethodResponse(response.methodResponses[0]);
429
+ if (!result.success) {
430
+ logger.error({ error: result.error, emailId }, 'JMAP error in move_email');
431
+ return {
432
+ isError: true,
433
+ content: [
434
+ {
435
+ type: 'text',
436
+ text: `Failed to move email: ${result.error?.description || result.error?.type || 'Unknown error'}`,
437
+ },
438
+ ],
439
+ };
440
+ }
441
+ const setResponse = result.data;
442
+ if (setResponse.notUpdated?.[emailId]) {
443
+ const error = setResponse.notUpdated[emailId];
444
+ logger.error({ error, emailId }, 'Email/set notUpdated in move_email');
445
+ return {
446
+ isError: true,
447
+ content: [
448
+ {
449
+ type: 'text',
450
+ text: `Failed to move email: ${error.type} - ${error.description || ''}`,
451
+ },
452
+ ],
453
+ };
454
+ }
455
+ logger.debug({ emailId, targetMailboxId }, 'move_email success');
456
+ return {
457
+ content: [
458
+ {
459
+ type: 'text',
460
+ text: JSON.stringify({ success: true, emailId, targetMailboxId }),
461
+ },
462
+ ],
463
+ };
464
+ }
465
+ catch (error) {
466
+ logger.error({ error, emailId }, 'Exception in move_email');
467
+ return {
468
+ isError: true,
469
+ content: [
470
+ {
471
+ type: 'text',
472
+ text: `Error moving email: ${error instanceof Error ? error.message : String(error)}`,
473
+ },
474
+ ],
475
+ };
476
+ }
477
+ });
478
+ // add_label - add a mailbox to an email without removing existing ones (EMAIL-09)
479
+ server.registerTool('add_label', {
480
+ title: 'Add Label to Email',
481
+ description: 'Add a label (mailbox) to an email without removing existing labels.',
482
+ inputSchema: {
483
+ emailId: z.string().describe('The unique identifier of the email'),
484
+ mailboxId: z.string().describe('The ID of the mailbox (label) to add'),
485
+ },
486
+ annotations: EMAIL_WRITE_ANNOTATIONS,
487
+ }, async ({ emailId, mailboxId }) => {
488
+ logger.debug({ emailId, mailboxId }, 'add_label called');
489
+ try {
490
+ const session = jmapClient.getSession();
491
+ const response = await jmapClient.request([
492
+ [
493
+ 'Email/set',
494
+ {
495
+ accountId: session.accountId,
496
+ update: {
497
+ [emailId]: {
498
+ [`mailboxIds/${mailboxId}`]: true,
499
+ },
500
+ },
501
+ },
502
+ 'addLabel',
503
+ ],
504
+ ]);
505
+ const result = jmapClient.parseMethodResponse(response.methodResponses[0]);
506
+ if (!result.success) {
507
+ logger.error({ error: result.error, emailId }, 'JMAP error in add_label');
508
+ return {
509
+ isError: true,
510
+ content: [
511
+ {
512
+ type: 'text',
513
+ text: `Failed to add label: ${result.error?.description || result.error?.type || 'Unknown error'}`,
514
+ },
515
+ ],
516
+ };
517
+ }
518
+ const setResponse = result.data;
519
+ if (setResponse.notUpdated?.[emailId]) {
520
+ const error = setResponse.notUpdated[emailId];
521
+ logger.error({ error, emailId }, 'Email/set notUpdated in add_label');
522
+ return {
523
+ isError: true,
524
+ content: [
525
+ {
526
+ type: 'text',
527
+ text: `Failed to add label: ${error.type} - ${error.description || ''}`,
528
+ },
529
+ ],
530
+ };
531
+ }
532
+ logger.debug({ emailId, mailboxId }, 'add_label success');
533
+ return {
534
+ content: [
535
+ {
536
+ type: 'text',
537
+ text: JSON.stringify({ success: true, emailId, addedMailboxId: mailboxId }),
538
+ },
539
+ ],
540
+ };
541
+ }
542
+ catch (error) {
543
+ logger.error({ error, emailId }, 'Exception in add_label');
544
+ return {
545
+ isError: true,
546
+ content: [
547
+ {
548
+ type: 'text',
549
+ text: `Error adding label: ${error instanceof Error ? error.message : String(error)}`,
550
+ },
551
+ ],
552
+ };
553
+ }
554
+ });
555
+ // remove_label - remove a mailbox from an email (EMAIL-10)
556
+ server.registerTool('remove_label', {
557
+ title: 'Remove Label from Email',
558
+ description: 'Remove a label (mailbox) from an email. Email must belong to at least one mailbox.',
559
+ inputSchema: {
560
+ emailId: z.string().describe('The unique identifier of the email'),
561
+ mailboxId: z.string().describe('The ID of the mailbox (label) to remove'),
562
+ },
563
+ annotations: EMAIL_WRITE_ANNOTATIONS,
564
+ }, async ({ emailId, mailboxId }) => {
565
+ logger.debug({ emailId, mailboxId }, 'remove_label called');
566
+ try {
567
+ const session = jmapClient.getSession();
568
+ const response = await jmapClient.request([
569
+ [
570
+ 'Email/set',
571
+ {
572
+ accountId: session.accountId,
573
+ update: {
574
+ [emailId]: {
575
+ [`mailboxIds/${mailboxId}`]: null,
576
+ },
577
+ },
578
+ },
579
+ 'removeLabel',
580
+ ],
581
+ ]);
582
+ const result = jmapClient.parseMethodResponse(response.methodResponses[0]);
583
+ if (!result.success) {
584
+ logger.error({ error: result.error, emailId }, 'JMAP error in remove_label');
585
+ return {
586
+ isError: true,
587
+ content: [
588
+ {
589
+ type: 'text',
590
+ text: `Failed to remove label: ${result.error?.description || result.error?.type || 'Unknown error'}`,
591
+ },
592
+ ],
593
+ };
594
+ }
595
+ const setResponse = result.data;
596
+ if (setResponse.notUpdated?.[emailId]) {
597
+ const error = setResponse.notUpdated[emailId];
598
+ logger.error({ error, emailId }, 'Email/set notUpdated in remove_label');
599
+ // Handle the case where email only has one mailbox
600
+ if (error.type === 'invalidProperties') {
601
+ return {
602
+ isError: true,
603
+ content: [
604
+ {
605
+ type: 'text',
606
+ text: 'Cannot remove label: email must belong to at least one mailbox',
607
+ },
608
+ ],
609
+ };
610
+ }
611
+ return {
612
+ isError: true,
613
+ content: [
614
+ {
615
+ type: 'text',
616
+ text: `Failed to remove label: ${error.type} - ${error.description || ''}`,
617
+ },
618
+ ],
619
+ };
620
+ }
621
+ logger.debug({ emailId, mailboxId }, 'remove_label success');
622
+ return {
623
+ content: [
624
+ {
625
+ type: 'text',
626
+ text: JSON.stringify({ success: true, emailId, removedMailboxId: mailboxId }),
627
+ },
628
+ ],
629
+ };
630
+ }
631
+ catch (error) {
632
+ logger.error({ error, emailId }, 'Exception in remove_label');
633
+ return {
634
+ isError: true,
635
+ content: [
636
+ {
637
+ type: 'text',
638
+ text: `Error removing label: ${error instanceof Error ? error.message : String(error)}`,
639
+ },
640
+ ],
641
+ };
642
+ }
643
+ });
644
+ // create_draft - create a new draft email (EMAIL-12)
645
+ server.registerTool('create_draft', {
646
+ title: 'Create Draft Email',
647
+ description: 'Create a new draft email in the Drafts mailbox for later editing or sending.',
648
+ inputSchema: {
649
+ to: z.array(z.string().email()).optional().describe('Recipient email addresses'),
650
+ cc: z.array(z.string().email()).optional().describe('CC email addresses'),
651
+ bcc: z.array(z.string().email()).optional().describe('BCC email addresses'),
652
+ subject: z.string().optional().describe('Email subject'),
653
+ body: z.string().optional().describe('Plain text email body'),
654
+ inReplyTo: z.string().optional().describe('Message-ID of the email being replied to'),
655
+ },
656
+ annotations: EMAIL_CREATE_ANNOTATIONS,
657
+ }, async ({ to, cc, bcc, subject, body, inReplyTo }) => {
658
+ logger.debug({ to, cc, bcc, subject, hasBody: !!body, inReplyTo }, 'create_draft called');
659
+ try {
660
+ const session = jmapClient.getSession();
661
+ // Get Drafts mailbox first (doesn't need submission capability)
662
+ const mailboxResponse = await jmapClient.request([
663
+ [
664
+ 'Mailbox/get',
665
+ {
666
+ accountId: session.accountId,
667
+ properties: ['id', 'role'],
668
+ },
669
+ 'getMailboxes',
670
+ ],
671
+ ]);
672
+ // Parse Mailbox response
673
+ const getResult = jmapClient.parseMethodResponse(mailboxResponse.methodResponses[0]);
674
+ // Try to get identity separately (needs submission capability)
675
+ let identity;
676
+ try {
677
+ const identityResponse = await jmapClient.request([['Identity/get', { accountId: session.accountId }, 'getIdentity']], ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail', 'urn:ietf:params:jmap:submission']);
678
+ const identityResult = jmapClient.parseMethodResponse(identityResponse.methodResponses[0]);
679
+ if (identityResult.success) {
680
+ const identities = identityResult.data.list;
681
+ identity = identities?.[0];
682
+ }
683
+ }
684
+ catch {
685
+ // Identity fetch failed - draft will be created without from field
686
+ logger.debug('Could not fetch identity for draft - will create without from field');
687
+ }
688
+ if (!getResult.success) {
689
+ logger.error({ error: getResult.error }, 'JMAP error getting mailboxes');
690
+ return {
691
+ isError: true,
692
+ content: [
693
+ {
694
+ type: 'text',
695
+ text: 'Failed to get mailboxes',
696
+ },
697
+ ],
698
+ };
699
+ }
700
+ const mailboxes = getResult.data.list;
701
+ const draftsMailbox = mailboxes.find((mb) => mb.role === 'drafts');
702
+ if (!draftsMailbox) {
703
+ logger.error({}, 'No Drafts mailbox found');
704
+ return {
705
+ isError: true,
706
+ content: [
707
+ {
708
+ type: 'text',
709
+ text: 'No Drafts mailbox found. Cannot create draft.',
710
+ },
711
+ ],
712
+ };
713
+ }
714
+ const draftsMailboxId = draftsMailbox.id;
715
+ // Build email create object
716
+ // Use textBody instead of bodyStructure for better server compatibility
717
+ const emailCreate = {
718
+ mailboxIds: { [draftsMailboxId]: true },
719
+ keywords: { '$draft': true, '$seen': true },
720
+ subject: subject || '',
721
+ textBody: [
722
+ {
723
+ partId: '1',
724
+ type: 'text/plain',
725
+ },
726
+ ],
727
+ bodyValues: {
728
+ '1': {
729
+ value: body || '',
730
+ },
731
+ },
732
+ };
733
+ // Set from field using identity (if available)
734
+ if (identity) {
735
+ emailCreate.from = [{ email: identity.email, name: identity.name }];
736
+ }
737
+ // Add optional address fields
738
+ if (to && to.length > 0) {
739
+ emailCreate.to = to.map((email) => ({ email }));
740
+ }
741
+ if (cc && cc.length > 0) {
742
+ emailCreate.cc = cc.map((email) => ({ email }));
743
+ }
744
+ if (bcc && bcc.length > 0) {
745
+ emailCreate.bcc = bcc.map((email) => ({ email }));
746
+ }
747
+ if (inReplyTo) {
748
+ emailCreate.inReplyTo = [inReplyTo];
749
+ }
750
+ // Create the draft
751
+ const createResponse = await jmapClient.request([
752
+ [
753
+ 'Email/set',
754
+ {
755
+ accountId: session.accountId,
756
+ create: {
757
+ draft: emailCreate,
758
+ },
759
+ },
760
+ 'createDraft',
761
+ ],
762
+ ]);
763
+ const createResult = jmapClient.parseMethodResponse(createResponse.methodResponses[0]);
764
+ if (!createResult.success) {
765
+ logger.error({ error: createResult.error }, 'JMAP error in create_draft');
766
+ return {
767
+ isError: true,
768
+ content: [
769
+ {
770
+ type: 'text',
771
+ text: `Failed to create draft: ${createResult.error?.description || createResult.error?.type || 'Unknown error'}`,
772
+ },
773
+ ],
774
+ };
775
+ }
776
+ const setResponse = createResult.data;
777
+ if (setResponse.notCreated?.draft) {
778
+ const error = setResponse.notCreated.draft;
779
+ logger.error({ error }, 'Email/set notCreated in create_draft');
780
+ return {
781
+ isError: true,
782
+ content: [
783
+ {
784
+ type: 'text',
785
+ text: `Failed to create draft: ${error.type} - ${error.description || ''}`,
786
+ },
787
+ ],
788
+ };
789
+ }
790
+ const created = setResponse.created?.draft;
791
+ if (!created) {
792
+ logger.error({}, 'No created draft in response');
793
+ return {
794
+ isError: true,
795
+ content: [
796
+ {
797
+ type: 'text',
798
+ text: 'Failed to create draft: no created email in response',
799
+ },
800
+ ],
801
+ };
802
+ }
803
+ logger.debug({ draftId: created.id, threadId: created.threadId }, 'create_draft success');
804
+ return {
805
+ content: [
806
+ {
807
+ type: 'text',
808
+ text: JSON.stringify({ success: true, draftId: created.id, threadId: created.threadId }),
809
+ },
810
+ ],
811
+ };
812
+ }
813
+ catch (error) {
814
+ logger.error({ error }, 'Exception in create_draft');
815
+ return {
816
+ isError: true,
817
+ content: [
818
+ {
819
+ type: 'text',
820
+ text: `Error creating draft: ${error instanceof Error ? error.message : String(error)}`,
821
+ },
822
+ ],
823
+ };
824
+ }
825
+ });
826
+ logger.debug('Email operation tools registered: mark_as_read, mark_as_unread, delete_email, move_email, add_label, remove_label, create_draft');
827
+ }
828
+ //# sourceMappingURL=email-operations.js.map