check_mail_account_config
check_mail_account_config
Validates email account configuration by checking IMAP/SMTP server connectivity and settings in Apple Mail or Outlook. Use at the start of any email troubleshooting workflow.
Metadata
Name
check_mail_account_config
Updated
3 weeks ago
Source
Risk
Low
Requires consent
false
Affected scope
user
Code
/**
* mcp/skills/checkMailAccountConfig.ts — check_mail_account_config skill
*
* Validates email account configuration by checking IMAP/SMTP server
* connectivity and settings in Apple Mail or Outlook.
*
* Platform strategy
* -----------------
* darwin Mail: enumerate accounts via Mail's AppleScript interface (JXA).
* Modern macOS (since ~OS X Yosemite) no longer stores accounts
* in ~/Library/Mail/.../Accounts.plist — they live in the system
* Accounts DB owned by accountsd and are exposed through Mail's
* scripting dictionary. This also covers OAuth providers (Gmail,
* Microsoft 365) added via Internet Accounts and needs only the
* Apple Events automation grant (Tier-1 auto-prompt), not FDA.
* darwin Outlook: read Outlook 15 Profiles via plutil
* win32: PowerShell check Outlook profile via registry
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/checkMailAccountConfig.ts
*/
import * as os from "os";
import * as nodePath from "path";
import { exec } from "child_process";
import { promisify } from "util";
import { z } from "zod";
import * as fs from "fs/promises";
const execAsync = promisify(exec);
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "check_mail_account_config",
description:
"Validates email account configuration by checking IMAP/SMTP server " +
"connectivity and settings in Apple Mail or Outlook. Use at the start of " +
"any email troubleshooting workflow.",
riskLevel: "low",
destructive: false,
requiresConsent: false,
supportsDryRun: false,
affectedScope: ["user"],
auditRequired: false,
schema: {
client: z
.enum(["mail", "outlook", "auto"])
.optional()
.describe("Email client to check. auto=detect installed client. Default: auto"),
account: z
.string()
.optional()
.describe("Email address to check. Omit to list all configured accounts"),
},
} as const;
// -- Types --------------------------------------------------------------------
interface AccountInfo {
email: string;
imapServer: string | null;
smtpServer: string | null;
port: number | null;
ssl: boolean | null;
}
type DetectedClient = "mail" | "outlook" | "unknown";
// -- PowerShell helper --------------------------------------------------------
async function runPS(script: string): Promise<string> {
const encoded = Buffer.from(script, "utf16le").toString("base64");
const { stdout } = await execAsync(
`powershell.exe -NoProfile -NonInteractive -EncodedCommand ${encoded}`,
{ maxBuffer: 10 * 1024 * 1024 },
);
return stdout.trim();
}
// -- darwin helpers -----------------------------------------------------------
async function readPlistAsJson(plistPath: string): Promise<Record<string, unknown> | null> {
try {
const { stdout } = await execAsync(
`plutil -convert json -o - '${plistPath.replace(/'/g, `'\\''`)}' 2>/dev/null`,
{ maxBuffer: 5 * 1024 * 1024 },
);
return JSON.parse(stdout) as Record<string, unknown>;
} catch {
return null;
}
}
// JXA enumerated against Mail's scripting dictionary. Returns a JSON array of
// account records, or {error} if Mail itself rejects the request. Per-property
// try/catch keeps one unreadable field from dropping the whole account.
const MAIL_JXA = `
(() => {
const Mail = Application("Mail");
let accts;
try { accts = Mail.accounts(); } catch (e) { return JSON.stringify({ error: String(e) }); }
const out = [];
for (const a of accts) {
let enabled = true; try { enabled = a.enabled(); } catch (e) {}
if (enabled === false) continue;
let emails = []; try { emails = a.emailAddresses() || []; } catch (e) {}
let userName = ""; try { userName = a.userName() || ""; } catch (e) {}
let server = null; try { server = a.serverName(); } catch (e) {}
let port = null; try { port = a.port(); } catch (e) {}
let ssl = null; try { ssl = a.usesSsl(); } catch (e) {}
let smtp = null; try { const d = a.deliveryAccount(); if (d) smtp = d.serverName(); } catch (e) {}
out.push({
email: emails[0] || userName || null,
imapServer: server || null,
smtpServer: smtp || null,
port: (typeof port === "number") ? port : null,
ssl: (typeof ssl === "boolean") ? ssl : null,
});
}
return JSON.stringify(out);
})();
`;
async function runJxa(script: string): Promise<string> {
const tmp = nodePath.join(os.tmpdir(), `mailcfg-${process.pid}-${Date.now()}.js`);
await fs.writeFile(tmp, script, "utf8");
try {
const { stdout } = await execAsync(`osascript -l JavaScript '${tmp}'`, {
timeout: 30_000,
maxBuffer: 5 * 1024 * 1024,
});
return stdout.trim();
} finally {
await fs.unlink(tmp).catch(() => undefined);
}
}
async function getMailAccounts(filterEmail?: string): Promise<{ accounts: AccountInfo[]; error: string | null }> {
let raw: string;
try {
raw = await runJxa(MAIL_JXA);
} catch (err) {
const msg = (err as { stderr?: string; message?: string }).stderr
|| (err instanceof Error ? err.message : String(err));
if (/-1743|Not authori[sz]ed|not allowed to send Apple events/i.test(msg)) {
return {
accounts: [],
error: "Automation permission required: allow this app to control Mail "
+ "(System Settings → Privacy & Security → Automation), then retry.",
};
}
return { accounts: [], error: `Could not read Mail accounts: ${msg.split("\n")[0]}` };
}
if (!raw) return { accounts: [], error: null };
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
return { accounts: [], error: null };
}
if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && "error" in parsed) {
return { accounts: [], error: String((parsed as { error: unknown }).error) };
}
const rawAccounts = Array.isArray(parsed) ? (parsed as Record<string, unknown>[]) : [];
const accounts: AccountInfo[] = [];
for (const acct of rawAccounts) {
const email = String(acct["email"] ?? "");
if (!email) continue;
if (filterEmail && !email.toLowerCase().includes(filterEmail.toLowerCase())) continue;
accounts.push({
email,
imapServer: (acct["imapServer"] as string) || null,
smtpServer: (acct["smtpServer"] as string) || null,
port: typeof acct["port"] === "number" ? (acct["port"] as number) : null,
ssl: typeof acct["ssl"] === "boolean" ? (acct["ssl"] as boolean) : null,
});
}
return { accounts, error: null };
}
async function getOutlookAccountsDarwin(filterEmail?: string): Promise<AccountInfo[]> {
const profileBase = nodePath.join(
os.homedir(),
"Library",
"Group Containers",
"UBF8T346G9.Office",
"Outlook",
"Outlook 15 Profiles",
);
let profileDirs: string[] = [];
try {
profileDirs = await fs.readdir(profileBase);
} catch {
return [];
}
const accounts: AccountInfo[] = [];
for (const profileDir of profileDirs) {
const plistPath = nodePath.join(profileBase, profileDir, "Account Profile.plist");
const data = await readPlistAsJson(plistPath);
if (!data) continue;
const accts = (data["Accounts"] ?? []) as Record<string, unknown>[];
for (const acct of accts) {
const email = String(acct["Email Address"] ?? acct["AccountEmailAddress"] ?? "");
const imapServer = String(acct["IMAP Server"] ?? acct["Hostname"] ?? "");
const smtpServer = String(acct["SMTP Server"] ?? "");
const port = acct["IMAP Port"] ? Number(acct["IMAP Port"]) : null;
const ssl = acct["Use SSL"] !== undefined ? Boolean(acct["Use SSL"]) : null;
if (!email) continue;
if (filterEmail && !email.toLowerCase().includes(filterEmail.toLowerCase())) continue;
accounts.push({ email, imapServer: imapServer || null, smtpServer: smtpServer || null, port, ssl });
}
}
return accounts;
}
// NOTE: client detection on darwin is intentionally NOT by app presence.
// Mail.app ALWAYS exists on macOS (system app at /System/Applications), so its
// presence cannot distinguish a Mail user from an Outlook user — presence-based
// detection resolved `client` to "mail" for every Mac and left the Outlook path
// unreachable (e.g. for an Outlook user whose Mail.app has no accounts). run()
// instead probes BOTH clients and resolves to whichever has accounts configured.
// -- win32 implementation -----------------------------------------------------
async function getAccountsWin32(filterEmail?: string): Promise<AccountInfo[]> {
const ps = `
$ErrorActionPreference = 'SilentlyContinue'
$profiles = Get-ChildItem 'HKCU:\Software\Microsoft\Office\16.0\Outlook\Profiles' -ErrorAction SilentlyContinue
$results = @()
foreach ($profile in $profiles) {
$accounts = Get-ChildItem $profile.PSPath -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.GetValue('Account Name') -ne $null }
foreach ($acct in $accounts) {
$results += [PSCustomObject]@{
email = [string]( if ($acct.GetValue('Email')) { $acct.GetValue('Email') } elseif ($acct.GetValue('Account Name')) { $acct.GetValue('Account Name') } else { '' } )
imapServer = [string]( if ($acct.GetValue('IMAP Server')) { $acct.GetValue('IMAP Server') } else { '' } )
smtpServer = [string]( if ($acct.GetValue('SMTP Server')) { $acct.GetValue('SMTP Server') } else { '' } )
port = $null
ssl = $null
}
}
}
$results | ConvertTo-Json -Depth 2 -Compress`.trim();
const raw = await runPS(ps);
if (!raw) return [];
let all: AccountInfo[];
try {
const parsed = JSON.parse(raw) as AccountInfo | AccountInfo[];
all = (Array.isArray(parsed) ? parsed : [parsed])
.filter(a => a.email);
} catch {
return [];
}
if (filterEmail) {
return all.filter(a => a.email.toLowerCase().includes(filterEmail.toLowerCase()));
}
return all;
}
// -- Exported run function ----------------------------------------------------
export async function run({
client = "auto",
account,
}: {
client?: "mail" | "outlook" | "auto";
account?: string;
} = {}) {
const platform = os.platform();
if (platform === "win32") {
const accounts = await getAccountsWin32(account);
return { client: "outlook", platform, accounts };
}
// macOS. In "auto" mode, resolve the client by which one actually has accounts
// configured — NOT by app presence (Mail.app always exists on macOS, so
// presence always picked "mail" and left "outlook" unreachable).
let resolvedClient: DetectedClient;
let accounts: AccountInfo[] = [];
let mailError: string | null = null;
if (client === "mail") {
const r = await getMailAccounts(account);
accounts = r.accounts;
mailError = r.error;
resolvedClient = "mail";
} else if (client === "outlook") {
accounts = await getOutlookAccountsDarwin(account);
resolvedClient = "outlook";
} else {
// auto: probe BOTH, resolve to whichever has accounts.
const mail = await getMailAccounts(account);
const outlookAccounts = await getOutlookAccountsDarwin(account);
accounts = [...mail.accounts, ...outlookAccounts];
mailError = mail.error;
resolvedClient = mail.accounts.length > 0
? "mail"
: outlookAccounts.length > 0 ? "outlook" : "unknown";
}
return { client: resolvedClient, platform, accounts, ...(mailError ? { error: mailError } : {}) };
}
// -- Smoke test ---------------------------------------------------------------
if (false) {
run({})
.then(r => console.log(JSON.stringify(r, null, 2)))
.catch((err: Error) => { console.error(err.message); process.exit(1); });
}