@lengelhard/imap-email-mcp 1.7.3 → 1.7.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.7.4
4
+
5
+ ### Added
6
+ - `search_index` results carry `index.warning` when the index may be behind: the last full sync finished more than 15 minutes ago (for example "index last synced 3 hours ago, results may be missing recent mail"), the index has never completed a full sync, or a searched folder recorded an error. The existing `index` fields are unchanged. This makes a stalled background sync visible in every search instead of silent, following the overnight freeze of 18 Sep 2026.
7
+
3
8
  ## 1.7.3
4
9
 
5
10
  ### Fixed
package/README.md CHANGED
@@ -136,7 +136,7 @@ Some IMAP servers return incomplete results for body searches without reporting
136
136
  | `list_emails` | List emails newest first, with unread, flagged and date filters and paging |
137
137
  | `get_email` | Get an email by UID. Text by default; HTML (without inline images) on request |
138
138
  | `search_emails` | Live IMAP search across one or more folders: subject, from, to, cc, body, text, dates, flags |
139
- | `search_index` | Full-text search of the local index (when `INDEX_ENABLED=true`) |
139
+ | `search_index` | Full-text search of the local index (when `INDEX_ENABLED=true`); `index.warning` flags a stale index or folder errors |
140
140
  | `index_status` | Per-folder index coverage, last sync and errors |
141
141
  | `sync_index` | Run an index sync now |
142
142
  | `list_drafts` | List all draft emails |
package/index.js CHANGED
@@ -378,7 +378,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
378
378
  ...(localIndex ? [
379
379
  {
380
380
  name: 'search_index',
381
- description: 'Full-text search of the local read-only mail index (subject, sender, recipients, attachment names and body text). Accent-insensitive: "Loic" matches "Loïc". Use this for content searches. Results carry folder and uid for get_email; a uid can be stale if the message was moved since the last sync, in which case get_email reports it as not found. Check index.complete in the response before treating zero results as proof that nothing exists.',
381
+ description: 'Full-text search of the local read-only mail index (subject, sender, recipients, attachment names and body text). Accent-insensitive: "Loic" matches "Loïc". Use this for content searches. Results carry folder and uid for get_email; a uid can be stale if the message was moved since the last sync, in which case get_email reports it as not found. Check index.complete in the response before treating zero results as proof that nothing exists. index.warning is present when the last full sync is older than 15 minutes or a searched folder has an error; pass it on to the user.',
382
382
  inputSchema: {
383
383
  type: 'object',
384
384
  properties: {
@@ -28,6 +28,16 @@ const LOCK_HEARTBEAT_MS = 30 * 1000;
28
28
  const REQUEST_RETRY_MS = 15 * 1000;
29
29
  const STALL_MS = 5 * 60 * 1000;
30
30
  const REQUEST_MAX_ATTEMPTS = 6;
31
+ const STALE_AFTER_MS = 15 * 60 * 1000;
32
+
33
+ function describeAge(ms) {
34
+ const minutes = Math.round(ms / 60000);
35
+ if (minutes < 60) return `${minutes} minute${minutes === 1 ? '' : 's'}`;
36
+ const hours = Math.round(ms / 3600000);
37
+ if (hours < 48) return `${hours} hour${hours === 1 ? '' : 's'}`;
38
+ const days = Math.round(ms / 86400000);
39
+ return `${days} day${days === 1 ? '' : 's'}`;
40
+ }
31
41
 
32
42
  class LockLostError extends Error {
33
43
  constructor() {
@@ -637,6 +647,17 @@ export class LocalIndex {
637
647
 
638
648
  const st = this.status();
639
649
  const scope = folderList.length ? st.folders.filter((f) => folderList.includes(f.folder)) : st.folders;
650
+ const warnings = [];
651
+ const lastFullMs = Number(this._meta('last_full_sync') || 0);
652
+ if (!lastFullMs) {
653
+ warnings.push('index has never completed a full sync, results may be missing mail');
654
+ } else if (Date.now() - lastFullMs > STALE_AFTER_MS) {
655
+ warnings.push(`index last synced ${describeAge(Date.now() - lastFullMs)} ago, results may be missing recent mail`);
656
+ }
657
+ const errored = scope.filter((f) => f.last_error);
658
+ if (errored.length) {
659
+ warnings.push(`folder error${errored.length === 1 ? '' : 's'}: ${errored.map((f) => `${f.folder} (${f.last_error})`).join(', ')}; results from ${errored.length === 1 ? 'that folder' : 'those folders'} may be incomplete`);
660
+ }
640
661
  return {
641
662
  total_matches: total,
642
663
  returned: rows.length,
@@ -645,7 +666,8 @@ export class LocalIndex {
645
666
  complete: scope.length > 0 && scope.every((f) => f.complete && !f.last_error),
646
667
  sync_running: st.sync_running,
647
668
  oldest_folder_sync: scope.reduce((min, f) => (f.last_sync && (!min || f.last_sync < min) ? f.last_sync : min), null),
648
- incomplete_folders: scope.filter((f) => !f.complete || f.last_error).map((f) => f.folder)
669
+ incomplete_folders: scope.filter((f) => !f.complete || f.last_error).map((f) => f.folder),
670
+ ...(warnings.length && { warning: warnings.join('; ') })
649
671
  },
650
672
  results: rows.map((r) => ({
651
673
  folder: r.folder,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lengelhard/imap-email-mcp",
3
- "version": "1.7.3",
3
+ "version": "1.7.4",
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",