rebuild_mail_index
rebuild_mail_index
Triggers Apple Mail mailbox index rebuild by removing the envelope index file, forcing Mail to rebuild on next launch. Use when Mail is slow, showing wrong message counts, or missing messages. macOS only.
Metadata
Name
rebuild_mail_index
Updated
3 weeks ago
Source
Risk
Medium
Requires consent
true
Affected scope
user
Code
/**
* mcp/skills/rebuildMailIndex.ts — rebuild_mail_index skill
*
* Triggers Apple Mail mailbox index rebuild by removing the envelope index
* file, forcing Mail to rebuild on next launch. Use when Mail is slow,
* showing wrong message counts, or missing messages.
*
* Platform strategy
* -----------------
* darwin Removes ~/Library/Mail/V10/MailData/Envelope Index (+ -shm, -wal)
* after optionally quitting Mail via AppleScript.
* win32 Not supported — Apple Mail is macOS only.
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/rebuildMailIndex.ts
*/
import * as os from "os";
import * as nodePath from "path";
import * as fs from "fs/promises";
import { exec } from "child_process";
import { promisify } from "util";
import { z } from "zod";
const execAsync = promisify(exec);
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "rebuild_mail_index",
description:
"Triggers Apple Mail mailbox index rebuild by removing the envelope index " +
"file, forcing Mail to rebuild on next launch. " +
"Use when Mail is slow, showing wrong message counts, or missing messages. " +
"macOS only.",
riskLevel: "medium",
destructive: true,
requiresConsent: true,
supportsDryRun: true,
affectedScope: ["user"],
auditRequired: true,
tccCategories: ["FullDiskAccess", "Automation"],
schema: {
dryRun: z
.boolean()
.optional()
.describe("If true, show what would be removed without removing. Default: true"),
},
} as const;
// -- Types --------------------------------------------------------------------
interface RebuildMailIndexResult {
filesFound: string[];
filesRemoved: string[];
mailWasRunning: boolean;
tccBlocked: boolean;
dryRun: boolean;
message: string;
}
// -- darwin implementation ----------------------------------------------------
async function rebuildMailIndexDarwin(dryRun: boolean): Promise<RebuildMailIndexResult> {
const home = os.homedir();
// Check if Mail is running
let mailWasRunning = false;
try {
const { stdout } = await execAsync("pgrep -x Mail", { timeout: 3_000 });
mailWasRunning = stdout.trim().length > 0;
} catch {
mailWasRunning = false;
}
// Quit Mail if running
if (mailWasRunning && !dryRun) {
try {
await execAsync("osascript -e 'tell application \"Mail\" to quit'", { timeout: 10_000 });
await new Promise((resolve) => setTimeout(resolve, 2000));
} catch {
}
}
let tccBlocked = false;
const isPermDenied = (e: unknown) => {
const code = (e as NodeJS.ErrnoException)?.code;
return code === "EPERM" || code === "EACCES";
};
const filesFound: string[] = [];
for (const version of ["V10", "V9", "V8"]) {
const baseDir = nodePath.join(home, "Library", "Mail", version, "MailData");
const variants = [
nodePath.join(baseDir, "Envelope Index"),
nodePath.join(baseDir, "Envelope Index-shm"),
nodePath.join(baseDir, "Envelope Index-wal"),
];
for (const f of variants) {
try {
await fs.access(f);
filesFound.push(f);
} catch (err) {
if (isPermDenied(err)) tccBlocked = true;
}
}
if (filesFound.length > 0) break;
}
const filesRemoved: string[] = [];
if (!dryRun) {
for (const f of filesFound) {
try {
await fs.unlink(f);
filesRemoved.push(f);
} catch (err) {
if (isPermDenied(err)) tccBlocked = true;
}
}
}
const tccMsg =
"macOS denied access to ~/Library/Mail (Full Disk Access not granted). " +
"Grant it in System Settings → Privacy & Security → Full Disk Access, then retry.";
const message = tccBlocked
? (dryRun
? `Could not read the Mail index — ${tccMsg}`
: `Could not remove the Mail index — ${tccMsg}`)
: dryRun
? filesFound.length > 0
? `Found ${filesFound.length} envelope index file(s). Run with dryRun=false to remove them and trigger a rebuild.`
: "No envelope index files found. Mail index may already be absent or stored in an unexpected location."
: filesRemoved.length > 0
? `Removed ${filesRemoved.length} file(s). Mail will rebuild its index on next launch.`
: "No files were removed. Check that Mail is not running and the files exist.";
return { filesFound, filesRemoved, mailWasRunning, tccBlocked, dryRun, message };
}
// -- win32 implementation -----------------------------------------------------
async function rebuildMailIndexWin32(_dryRun: boolean): Promise<RebuildMailIndexResult> {
return {
filesFound: [],
filesRemoved: [],
mailWasRunning: false,
tccBlocked: false,
dryRun: _dryRun,
message:
"Apple Mail is macOS only. For Outlook on Windows, use repair_outlook_database.",
};
}
// -- Exported run function ----------------------------------------------------
export async function run({
dryRun = true,
}: {
dryRun?: boolean;
} = {}) {
const platform = os.platform();
return platform === "win32"
? rebuildMailIndexWin32(dryRun)
: rebuildMailIndexDarwin(dryRun);
}
// -- Smoke test ---------------------------------------------------------------
if (false) {
run({})
.then(r => console.log(JSON.stringify(r, null, 2)))
.catch((err: Error) => { console.error(err.message); process.exit(1); });
}