Code

/**
 * mcp/skills/checkMailPermissions.ts — check_mail_permissions skill
 *
 * Checks file system permissions on the Mail data directory.
 * Incorrect permissions cause Mail sync failures and index corruption.
 * macOS only.
 *
 * Platform strategy
 * -----------------
 * darwin  Checks ~/Library/Mail for existence and R/W access.
 *         Parses ownership from `ls -la ~/Library/` and optionally repairs
 *         with `chown -R $(whoami) ~/Library/Mail`.
 * win32   Not supported — use check_mail_account_config for Windows diagnostics.
 *
 * Smoke test
 *   npx tsx -r dotenv/config mcp/skills/checkMailPermissions.ts
 */
 
import * as os       from "os";
import * as nodePath from "path";
import * as fs       from "fs";
import { exec }      from "child_process";
import { promisify } from "util";
import { z }         from "zod";
 
const execAsync = promisify(exec);
 
// -- Meta ---------------------------------------------------------------------
 
export const meta = {
  name: "check_mail_permissions",
  description:
    "Checks file system permissions on the Mail data directory. " +
    "Incorrect permissions cause Mail sync failures and index corruption. " +
    "macOS only.",
  riskLevel:       "low",
  destructive:     false,
  requiresConsent: false,
  supportsDryRun:  false,
  affectedScope:   ["user"],
  auditRequired:   false,
  schema: {
    fix: z
      .boolean()
      .optional()
      .describe(
        "If true, attempt to repair permissions with chown/chmod. Default: false",
      ),
  },
} as const;
 
// -- Types --------------------------------------------------------------------
 
interface CheckMailPermissionsResult {
  mailDir:        string;
  exists:         boolean;
  readable:       boolean;
  writable:       boolean;
  owner:          string | null;
  currentUser:    string;
  permissionsOk:  boolean;
  fixed:          boolean;
  supported?:     boolean;
  message:        string;
}
 
// -- darwin implementation ----------------------------------------------------
 
async function checkMailPermissionsDarwin(fix: boolean): Promise<CheckMailPermissionsResult> {
  const home    = os.homedir();
  const mailDir = nodePath.join(home, "Library", "Mail");
 
  // Get current user
  let currentUser = "unknown";
  try {
    const { stdout } = await execAsync("whoami");
    currentUser = stdout.trim();
  } catch {
    // Fallback to os.userInfo
    try { currentUser = os.userInfo().username; } catch { /* ignore */ }
  }
 
  // Check if Mail directory exists
  let exists = false;
  try {
    await fs.promises.access(mailDir);
    exists = true;
  } catch {
    exists = false;
  }
 
  if (!exists) {
    return {
      mailDir,
      exists:        false,
      readable:      false,
      writable:      false,
      owner:         null,
      currentUser,
      permissionsOk: false,
      fixed:         false,
      message:       `Mail directory not found at ${mailDir}. Apple Mail may not be configured.`,
    };
  }
 
  // Check read/write access
  let readable = false;
  let writable = false;
  try {
    await fs.promises.access(mailDir, fs.constants.R_OK);
    readable = true;
  } catch { /* no read access */ }
  try {
    await fs.promises.access(mailDir, fs.constants.W_OK);
    writable = true;
  } catch { /* no write access */ }
 
  // Parse ownership from `ls -la ~/Library/`
  let owner: string | null = null;
  try {
    const { stdout } = await execAsync(`ls -la '${home.replace(/'/g, "'\\''")}/Library/'`);
    const lines = stdout.split("\n");
    for (const line of lines) {
      // Match "Mail" entry — look for the entry ending in " Mail" or " Mail/"
      if (/\bMail\/?$/.test(line.trim())) {
        const parts = line.trim().split(/\s+/);
        // ls -la: permissions links owner group size date... name
        if (parts.length >= 4) {
          owner = parts[2]; // owner field
        }
        break;
      }
    }
  } catch {
    // Non-fatal — owner stays null
  }
 
  const permissionsOk = readable && writable && (owner === null || owner === currentUser);
 
  let fixed = false;
  if (fix && (!readable || !writable || (owner !== null && owner !== currentUser))) {
    try {
      await execAsync(`chown -R $(whoami) '${mailDir.replace(/'/g, "'\\''")}'`);
      fixed = true;
    } catch {
      // Fix failed — non-fatal
    }
  }
 
  const message = fixed
    ? "Permissions repaired with chown -R. Restart Mail to apply changes."
    : permissionsOk
      ? "Mail directory permissions look correct."
      : `Permission issues detected — readable:${readable}, writable:${writable}, owner:${owner ?? "unknown"} vs currentUser:${currentUser}. Run with fix=true to attempt repair.`;
 
  return { mailDir, exists, readable, writable, owner, currentUser, permissionsOk, fixed, message };
}
 
// -- win32 implementation -----------------------------------------------------
 
async function checkMailPermissionsWin32(_fix: boolean): Promise<CheckMailPermissionsResult> {
  return {
    mailDir:       "",
    exists:        false,
    readable:      false,
    writable:      false,
    owner:         null,
    currentUser:   "",
    permissionsOk: false,
    fixed:         false,
    supported:     false,
    message:       "Use check_mail_account_config for Windows Outlook diagnostics.",
  };
}
 
// -- Exported run function ----------------------------------------------------
 
export async function run({
  fix = false,
}: {
  fix?: boolean;
} = {}) {
  const platform = os.platform();
  return platform === "win32"
    ? checkMailPermissionsWin32(fix)
    : checkMailPermissionsDarwin(fix);
}
 
// -- Smoke test ---------------------------------------------------------------
 
if (false) {
  run({})
    .then(r => console.log(JSON.stringify(r, null, 2)))
    .catch((err: Error) => { console.error(err.message); process.exit(1); });
}