/**
* 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: read ~/Library/Mail/V{n}/MailData/Accounts.plist via plutil
* 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 findMailDataDir () : Promise < string | null > {
const mailBase = nodePath. join (os. homedir (), "Library" , "Mail" );
try {
const entries = await fs. readdir (mailBase);
// Prefer highest version number (V10, V9, ...)
const vDirs = entries
. filter ( e => / ^ V \d +$ / . test (e))
. sort (( a , b ) => parseInt (b. slice ( 1 ), 10 ) - parseInt (a. slice ( 1 ), 10 ));
if (vDirs. length > 0 ) {
return nodePath. join (mailBase, vDirs[ 0 ], "MailData" );
}
} catch {
// Mail directory not found
}
return null ;
}
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 ;
}
}
async function getMailAccounts ( filterEmail ?: string ) : Promise < AccountInfo []> {
const mailDataDir = await findMailDataDir ();
if ( ! mailDataDir) return [];
const plistPath = nodePath. join (mailDataDir, "Accounts.plist" );
const data = await readPlistAsJson (plistPath);
if ( ! data) return [];
const rawAccounts = (data[ "MailAccounts" ] ?? data[ "Accounts" ] ?? []) as Record < string , unknown >[];
const accounts : AccountInfo [] = [];
for ( const acct of rawAccounts) {
const email = String (acct[ "EmailAddresses" ]
? (acct[ "EmailAddresses" ] as string [])[ 0 ] ?? ""
: acct[ "AccountEmailAddress" ] ?? "" );
const imapServer = String (acct[ "Hostname" ] ?? acct[ "IMAPHostName" ] ?? "" );
const smtpServer = String ((acct[ "SMTPAccount" ] as Record < string , unknown >)?.[ "Hostname" ] ?? "" );
const port = acct[ "PortNumber" ] ? Number (acct[ "PortNumber" ]) : null ;
const ssl = acct[ "SSLEnabled" ] !== undefined ? Boolean (acct[ "SSLEnabled" ]) : 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;
}
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;
}
async function detectClientDarwin () : Promise < DetectedClient > {
try {
await fs. access ( "/Applications/Mail.app" );
return "mail" ;
} catch {
// not found
}
try {
await fs. access ( "/Applications/Microsoft Outlook.app" );
return "outlook" ;
} catch {
// not found
}
return "unknown" ;
}
// -- 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]($acct.GetValue('Email') ?? $acct.GetValue('Account Name') ?? '')
imapServer = [string]($acct.GetValue('IMAP Server') ?? '')
smtpServer = [string]($acct.GetValue('SMTP Server') ?? '')
port = $null
ssl = $null
}
}
}
$results | ConvertTo-Json -Depth 2 -Compress` . trim ();
const raw = await runPS (ps);
if ( ! raw) return [];
const parsed = JSON . parse (raw) as AccountInfo | AccountInfo [];
const all = (Array. isArray (parsed) ? parsed : [parsed])
. filter ( a => a.email);
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
let resolvedClient : DetectedClient = client === "auto" ? await detectClientDarwin () : client;
let accounts : AccountInfo [] = [];
if (resolvedClient === "mail" ) {
accounts = await getMailAccounts (account);
} else if (resolvedClient === "outlook" ) {
accounts = await getOutlookAccountsDarwin (account);
} else {
// Try both
const mailAccounts = await getMailAccounts (account);
const outlookAccounts = await getOutlookAccountsDarwin (account);
accounts = [ ... mailAccounts, ... outlookAccounts];
resolvedClient = mailAccounts. length > 0 ? "mail" : outlookAccounts. length > 0 ? "outlook" : "unknown" ;
}
return { client: resolvedClient, platform, accounts };
}
// -- Smoke test ---------------------------------------------------------------
if ( false ) {
run ({})
. then ( r => console. log ( JSON . stringify (r, null , 2 )))
. catch (( err : Error ) => { console. error (err.message); process. exit ( 1 ); });
}