check_smtp_connectivity

check_smtp_connectivity

Tests TCP connectivity to an SMTP mail server on standard ports. Use when outgoing email fails or bounces to determine if the issue is network connectivity vs configuration.

Metadata

Name

check_smtp_connectivity

Updated

2 months ago

Source

GitHub source

Risk

Low

Requires consent

false

Affected scope

user

Code

/**
 * mcp/skills/checkSmtpConnectivity.ts — check_smtp_connectivity skill
 *
 * Tests TCP connectivity to an SMTP mail server on standard ports.
 * Use when outgoing email fails or bounces to determine if the issue is
 * network connectivity vs configuration.
 *
 * Platform strategy
 * -----------------
 * darwin & win32  Pure Node.js net.createConnection() — no child_process needed.
 *                 For each port, attempts a TCP connection and reads the
 *                 SMTP greeting banner (first 256 bytes).
 *
 * Smoke test
 *   npx tsx -r dotenv/config mcp/skills/checkSmtpConnectivity.ts
 */

import * as os  from "os";
import * as net from "net";
import { z }    from "zod";

// -- Meta ---------------------------------------------------------------------

export const meta = {
  name: "check_smtp_connectivity",
  description:
    "Tests TCP connectivity to an SMTP mail server on standard ports. " +
    "Use when outgoing email fails or bounces to determine if the issue is " +
    "network connectivity vs configuration.",
  riskLevel:       "low",
  destructive:     false,
  requiresConsent: false,
  supportsDryRun:  false,
  affectedScope:   ["user"],
  auditRequired:   false,
  schema: {
    host: z
      .string()
      .describe("SMTP server hostname (e.g. 'smtp.gmail.com', 'mail.company.com')"),
    ports: z
      .array(z.number())
      .optional()
      .describe("Ports to test. Default: [587, 465, 25]"),
    timeoutMs: z
      .number()
      .optional()
      .describe("Connection timeout per port in ms. Default: 5000"),
  },
} as const;

// -- Types --------------------------------------------------------------------

interface SmtpPortResult {
  port:        number;
  service:     string;
  reachable:   boolean;
  latencyMs:   number | null;
  banner:      string | null;
  error:       string | null;
}

// -- Helpers ------------------------------------------------------------------

function portService(port: number): string {
  switch (port) {
    case 587:  return "submission";
    case 465:  return "smtps";
    case 25:   return "smtp";
    default:   return "unknown";
  }
}

function testPort(host: string, port: number, timeoutMs: number): Promise<SmtpPortResult> {
  return new Promise((resolve) => {
    const service   = portService(port);
    const startedAt = Date.now();
    let   banner    = "";
    let   resolved  = false;

const finish = (result: SmtpPortResult) => {
      if (!resolved) {
        resolved = true;
        socket.destroy();
        resolve(result);
      }
    };

const socket = net.createConnection({ host, port });
    socket.setTimeout(timeoutMs);

socket.on("connect", () => {
      const latencyMs = Date.now() - startedAt;
      // Collect the banner — wait up to 1 second for data
      const bannerTimer = setTimeout(() => {
        finish({
          port,
          service,
          reachable:  true,
          latencyMs,
          banner:     banner.trim() || null,
          error:      null,
        });
      }, 1000);
      socket.once("data", (chunk) => {
        clearTimeout(bannerTimer);
        banner = chunk.slice(0, 256).toString("utf8").replace(/\r\n/g, " ").trim();
        finish({
          port,
          service,
          reachable:  true,
          latencyMs,
          banner:     banner || null,
          error:      null,
        });
      });
    });

socket.on("timeout", () => {
      finish({ port, service, reachable: false, latencyMs: null, banner: null, error: "Connection timed out" });
    });

socket.on("error", (err) => {
      finish({ port, service, reachable: false, latencyMs: null, banner: null, error: err.message });
    });
  });
}

// -- Exported run function ----------------------------------------------------

export async function run({
  host,
  ports     = [587, 465, 25],
  timeoutMs = 5000,
}: {
  host:       string;
  ports?:     number[];
  timeoutMs?: number;
}) {
  const results = await Promise.all(
    ports.map((port) => testPort(host, port, timeoutMs)),
  );
  const anyReachable = results.some((r) => r.reachable);
  return { host, results, anyReachable, platform: os.platform() };
}

// -- Smoke test ---------------------------------------------------------------

if (false) {
  run({ host: "smtp.gmail.com" })
    .then(r => console.log(JSON.stringify(r, null, 2)))
    .catch((err: Error) => { console.error(err.message); process.exit(1); });
}