@lengelhard/imap-email-mcp 1.0.4 → 1.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 (2) hide show
  1. package/index.js +184 -23
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -347,9 +347,28 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
347
347
  required: ['to', 'subject']
348
348
  }
349
349
  },
350
+ {
351
+ name: 'delete_email',
352
+ description: 'Delete an email by UID',
353
+ inputSchema: {
354
+ type: 'object',
355
+ properties: {
356
+ uid: {
357
+ type: 'number',
358
+ description: 'Email UID to delete'
359
+ },
360
+ folder: {
361
+ type: 'string',
362
+ description: 'Folder name (default: INBOX)',
363
+ default: 'INBOX'
364
+ }
365
+ },
366
+ required: ['uid']
367
+ }
368
+ },
350
369
  {
351
370
  name: 'move_email',
352
- description: 'Move an email from one folder to another (e.g. INBOX to Archive)',
371
+ description: 'Move an email from one folder to another (e.g. Inbox Action, Waiting, Delegated, Hold)',
353
372
  inputSchema: {
354
373
  type: 'object',
355
374
  properties: {
@@ -359,35 +378,61 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
359
378
  },
360
379
  from_folder: {
361
380
  type: 'string',
362
- description: 'Source folder (default: INBOX)',
381
+ description: 'Source folder name (default: INBOX)',
363
382
  default: 'INBOX'
364
383
  },
365
384
  to_folder: {
366
385
  type: 'string',
367
- description: 'Destination folder (e.g. Archive, Sent, Trash)',
368
- default: 'Archive'
386
+ description: 'Destination folder name (e.g. Action, Waiting, Delegated, Hold, Archive)'
369
387
  }
370
388
  },
371
- required: ['uid']
389
+ required: ['uid', 'to_folder']
372
390
  }
373
391
  },
374
392
  {
375
- name: 'delete_email',
376
- description: 'Delete an email by UID',
393
+ name: 'flag_email',
394
+ description: 'Add or remove the \\Flagged flag on an email (star/unstar). Used to manage the /Flagged urgency overlay.',
377
395
  inputSchema: {
378
396
  type: 'object',
379
397
  properties: {
380
398
  uid: {
381
399
  type: 'number',
382
- description: 'Email UID to delete'
400
+ description: 'Email UID'
383
401
  },
384
402
  folder: {
385
403
  type: 'string',
386
- description: 'Folder name (default: INBOX)',
404
+ description: 'Folder containing the email (default: INBOX)',
405
+ default: 'INBOX'
406
+ },
407
+ flagged: {
408
+ type: 'boolean',
409
+ description: 'true to add the flag (star), false to remove it (unstar)'
410
+ }
411
+ },
412
+ required: ['uid', 'flagged']
413
+ }
414
+ },
415
+ {
416
+ name: 'mark_seen',
417
+ description: 'Mark an email as read (seen) or unread (unseen). Used to signal triage state via the \\Seen flag.',
418
+ inputSchema: {
419
+ type: 'object',
420
+ properties: {
421
+ uid: {
422
+ type: 'number',
423
+ description: 'Email UID'
424
+ },
425
+ folder: {
426
+ type: 'string',
427
+ description: 'Folder containing the email (default: INBOX)',
387
428
  default: 'INBOX'
429
+ },
430
+ seen: {
431
+ type: 'boolean',
432
+ description: 'true to mark as read, false to mark as unread'
388
433
  }
389
434
  },
390
- required: ['uid']
435
+ required: ['uid', 'seen']
391
436
  }
392
437
  }
393
438
  ]
@@ -781,47 +826,149 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
781
826
  bcc: args.bcc
782
827
  };
783
828
 
829
+ // Send via SMTP and capture the raw RFC 5322 message
784
830
  const info = await transporter.sendMail(mailOptions);
785
831
 
832
+ // Build RFC 5322 message string to APPEND to Sent folder via IMAP
833
+ // Use the same content that was sent so the Sent copy is identical
834
+ const sentDate = new Date();
835
+ const boundary = `----=_Part_${Date.now()}`;
836
+ let rawMessage = '';
837
+ rawMessage += `From: ${SMTP_CONFIG.auth.user}\r\n`;
838
+ rawMessage += `To: ${args.to}\r\n`;
839
+ if (args.cc) rawMessage += `Cc: ${args.cc}\r\n`;
840
+ if (args.bcc) rawMessage += `Bcc: ${args.bcc}\r\n`;
841
+ rawMessage += `Subject: ${args.subject}\r\n`;
842
+ rawMessage += `Date: ${sentDate.toUTCString()}\r\n`;
843
+ rawMessage += `Message-ID: ${info.messageId}\r\n`;
844
+ rawMessage += `MIME-Version: 1.0\r\n`;
845
+
846
+ if (args.html) {
847
+ rawMessage += `Content-Type: multipart/alternative; boundary="${boundary}"\r\n\r\n`;
848
+ rawMessage += `--${boundary}\r\n`;
849
+ rawMessage += `Content-Type: text/plain; charset=utf-8\r\n\r\n`;
850
+ rawMessage += `${args.body || ''}\r\n`;
851
+ rawMessage += `--${boundary}\r\n`;
852
+ rawMessage += `Content-Type: text/html; charset=utf-8\r\n\r\n`;
853
+ rawMessage += `${args.html}\r\n`;
854
+ rawMessage += `--${boundary}--\r\n`;
855
+ } else {
856
+ rawMessage += `Content-Type: text/plain; charset=utf-8\r\n\r\n`;
857
+ rawMessage += `${args.body || ''}\r\n`;
858
+ }
859
+
860
+ // APPEND to Sent folder via IMAP
861
+ let sentFolderResult = 'skipped';
862
+ try {
863
+ const connection = await connectIMAP();
864
+ try {
865
+ // Try common Sent folder names in order
866
+ const sentFolderCandidates = ['Sent', 'INBOX.Sent', 'Sent Items', 'Sent Mail', '[Gmail]/Sent Mail'];
867
+ let sentFolder = 'Sent'; // default fallback
868
+
869
+ const folders = await connection.getBoxes();
870
+ const flatFolders = Object.keys(folders);
871
+ for (const candidate of sentFolderCandidates) {
872
+ if (flatFolders.some(f => f.toLowerCase() === candidate.toLowerCase())) {
873
+ sentFolder = candidate;
874
+ break;
875
+ }
876
+ }
877
+
878
+ await connection.append(rawMessage, {
879
+ mailbox: sentFolder,
880
+ flags: ['\\Seen']
881
+ });
882
+ sentFolderResult = `saved to ${sentFolder}`;
883
+ } finally {
884
+ connection.end();
885
+ }
886
+ } catch (appendErr) {
887
+ sentFolderResult = `failed: ${appendErr.message}`;
888
+ }
889
+
786
890
  return {
787
891
  content: [{
788
892
  type: 'text',
789
893
  text: JSON.stringify({
790
894
  success: true,
791
895
  messageId: info.messageId,
792
- response: info.response
896
+ response: info.response,
897
+ sentFolder: sentFolderResult
793
898
  }, null, 2)
794
899
  }]
795
900
  };
796
901
  }
797
902
 
903
+ case 'delete_email': {
904
+ const folder = args.folder || 'INBOX';
905
+ const connection = await connectIMAP();
906
+
907
+ try {
908
+ await connection.openBox(folder);
909
+ await connection.addFlags(args.uid, ['\\Deleted']);
910
+ await connection.closeBox(true); // Expunge
911
+
912
+ return { content: [{ type: 'text', text: 'Email deleted successfully' }] };
913
+ } finally {
914
+ connection.end();
915
+ }
916
+ }
917
+
798
918
  case 'move_email': {
799
919
  const fromFolder = args.from_folder || 'INBOX';
800
- const toFolder = args.to_folder || 'Archive';
920
+ const toFolder = args.to_folder;
801
921
  const connection = await connectIMAP();
802
922
 
803
923
  try {
804
924
  await connection.openBox(fromFolder);
805
925
 
806
- // imap-simple exposes the underlying imap connection via ._imap
807
- // Use it to copy the message then delete the original
926
+ // node-imap exposes move() on the underlying imap object.
927
+ // We wrap it in a Promise so it fits the async/await flow.
808
928
  await new Promise((resolve, reject) => {
809
- connection.imap.copy(args.uid, toFolder, (err) => {
929
+ connection.imap.move(args.uid, toFolder, (err) => {
810
930
  if (err) reject(err);
811
931
  else resolve();
812
932
  });
813
933
  });
814
934
 
815
- // Mark original as deleted and expunge
816
- await connection.addFlags(args.uid, ['\\Deleted']);
817
- await connection.closeBox(true); // true = expunge on close
935
+ return {
936
+ content: [{
937
+ type: 'text',
938
+ text: JSON.stringify({
939
+ success: true,
940
+ uid: args.uid,
941
+ from: fromFolder,
942
+ to: toFolder
943
+ }, null, 2)
944
+ }]
945
+ };
946
+ } finally {
947
+ connection.end();
948
+ }
949
+ }
950
+
951
+ case 'flag_email': {
952
+ const folder = args.folder || 'INBOX';
953
+ const connection = await connectIMAP();
954
+
955
+ try {
956
+ await connection.openBox(folder);
957
+
958
+ if (args.flagged) {
959
+ await connection.addFlags(args.uid, ['\\Flagged']);
960
+ } else {
961
+ await connection.delFlags(args.uid, ['\\Flagged']);
962
+ }
818
963
 
819
964
  return {
820
965
  content: [{
821
966
  type: 'text',
822
967
  text: JSON.stringify({
823
968
  success: true,
824
- message: `Email ${args.uid} moved from ${fromFolder} to ${toFolder}`
969
+ uid: args.uid,
970
+ folder,
971
+ flagged: args.flagged
825
972
  }, null, 2)
826
973
  }]
827
974
  };
@@ -830,16 +977,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
830
977
  }
831
978
  }
832
979
 
833
- case 'delete_email': {
980
+ case 'mark_seen': {
834
981
  const folder = args.folder || 'INBOX';
835
982
  const connection = await connectIMAP();
836
983
 
837
984
  try {
838
985
  await connection.openBox(folder);
839
- await connection.addFlags(args.uid, ['\\Deleted']);
840
- await connection.closeBox(true); // Expunge
841
986
 
842
- return { content: [{ type: 'text', text: 'Email deleted successfully' }] };
987
+ if (args.seen) {
988
+ await connection.addFlags(args.uid, ['\\Seen']);
989
+ } else {
990
+ await connection.delFlags(args.uid, ['\\Seen']);
991
+ }
992
+
993
+ return {
994
+ content: [{
995
+ type: 'text',
996
+ text: JSON.stringify({
997
+ success: true,
998
+ uid: args.uid,
999
+ folder,
1000
+ seen: args.seen
1001
+ }, null, 2)
1002
+ }]
1003
+ };
843
1004
  } finally {
844
1005
  connection.end();
845
1006
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lengelhard/imap-email-mcp",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for Claude Code that provides email capabilities through IMAP/SMTP. Read, search, compose, and manage emails from any IMAP provider.",
5
5
  "type": "module",
6
6
  "main": "index.js",