← All @molecule/* packages · App templates

@molecule/api-smtp

Utility · smtp · API (Node) · v1.0.1 · Apache-2.0

Direct SMTP send client — sends mail through a user-supplied SMTP server (distinct from transactional email provider bonds).

npm install @molecule/api-smtp

npm · Source on GitHub

How it works

@molecule/api-smtp is a utility package for the API (Node) side (smtp).

import { connectSmtp } from '@molecule/api-smtp'

const client = await connectSmtp({
  host: 'smtp.example.com',
  port: 587,
  secure: false,
  requireTLS: true,
  auth: { user: 'me@example.com', pass: 'app-password' },
})

await client.verify()
const result = await client.sendMail({
  from: 'me@example.com',
  to: 'friend@example.com',
  subject: 'hi',
  text: 'hello world',
})
await client.disconnect()

Reference

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Direct SMTP send client for molecule.dev.

Sends mail through a user-supplied SMTP server (host, port, credentials owned by the end-user — for example, the email-client flagship app where each user signs in to their own mailbox). This is intentionally distinct from the @molecule/api-emails-* bonds, which wrap transactional providers like Mailgun, SendGrid, and SES that send mail on the application's behalf.

Wraps nodemailer so consumers never import nodemailer types directly — swapping the underlying library would only require changes inside this package.

Quick Start

import { connectSmtp } from '@molecule/api-smtp'

const client = await connectSmtp({
  host: 'smtp.example.com',
  port: 587,
  secure: false,
  requireTLS: true,
  auth: { user: 'me@example.com', pass: 'app-password' },
})

await client.verify()
const result = await client.sendMail({
  from: 'me@example.com',
  to: 'friend@example.com',
  subject: 'hi',
  text: 'hello world',
})
await client.disconnect()

Type

utility

Installation

npm install @molecule/api-smtp nodemailer
npm install -D @types/nodemailer

API

Interfaces

ConnectSmtpInternals

Optional injection point used by tests to swap in a fake nodemailer implementation. Production callers never need this.

interface ConnectSmtpInternals {
  /**
   * Replacement for `nodemailer.createTransport`. Must return a
   * minimal `Transporter`-compatible object with `verify`,
   * `sendMail`, and `close` methods.
   */
  createTransport?: (
    options: SMTPTransport.Options,
  ) => Pick<Transporter<SMTPTransport.SentMessageInfo>, 'verify' | 'sendMail' | 'close'>
}

SendResult

Result of a successful {@link SmtpClient.sendMail} call.

interface SendResult {
  /** RFC-5322 `Message-ID` of the queued message. */
  messageId: string

  /** Recipient addresses the server accepted. */
  accepted: string[]

  /** Recipient addresses the server rejected. */
  rejected: string[]

  /** Raw final SMTP response line (e.g. `"250 2.0.0 OK ..."`). */
  response: string
}

SmtpAttachment

One MIME attachment to be sent as part of an {@link SmtpMessage}.

Either content (raw bytes / string) or path (filesystem path) must be provided — never both.

interface SmtpAttachment {
  /** Filename shown to the recipient. */
  filename: string

  /**
   * Raw attachment content. Use this OR {@link SmtpAttachment.path},
   * not both.
   */
  content?: string | Buffer | Uint8Array

  /**
   * Path to a local file. Use this OR
   * {@link SmtpAttachment.content}, not both.
   */
  path?: string

  /**
   * Optional MIME type. If omitted, nodemailer infers from the
   * filename extension.
   */
  contentType?: string
}

SmtpClient

Connected SMTP client. Created via {@link connectSmtp}.

interface SmtpClient {
  /**
   * Verify the connection / credentials by issuing a dry SMTP
   * handshake. Resolves on success, rejects with {@link SmtpError}
   * otherwise.
   */
  verify(): Promise<void>

  /**
   * Send a single mail message.
   *
   * @param message - Message to send.
   * @returns Normalized {@link SendResult}.
   * @throws {SmtpError} on connection / send failure.
   */
  sendMail(message: SmtpMessage): Promise<SendResult>

  /**
   * Close the connection pool and release sockets. Safe to call
   * multiple times.
   */
  disconnect(): Promise<void>
}

SmtpConfig

Connection + authentication parameters for {@link connectSmtp}.

auth may be null for unauthenticated relays (rare — typically only same-host development MTAs).

interface SmtpConfig {
  /** SMTP server hostname. */
  host: string

  /** SMTP server port — typically 465 (TLS), 587 (STARTTLS), or 25. */
  port: number

  /**
   * If `true`, use implicit TLS on connect (port 465 style). When
   * `false` or omitted, the connection starts plain and may upgrade
   * via STARTTLS depending on {@link SmtpConfig.requireTLS}.
   */
  secure?: boolean

  /**
   * Authentication credentials, or `null` for an unauthenticated
   * relay.
   */
  auth: SmtpPasswordAuth | SmtpOAuth2Auth | null

  /**
   * If `true`, refuse to send mail unless the connection has been
   * upgraded to TLS (via STARTTLS when `secure` is `false`). Defaults
   * to `false`.
   */
  requireTLS?: boolean

  /**
   * Connection timeout in milliseconds. Defaults to 30_000.
   */
  connectionTimeoutMs?: number

  /**
   * Socket idle timeout in milliseconds. Defaults to 30_000.
   */
  socketTimeoutMs?: number

  /**
   * Greeting timeout in milliseconds. Defaults to 30_000.
   */
  greetingTimeoutMs?: number
}

SmtpMessage

A single email message to send via {@link SmtpClient.sendMail}.

Address fields accept either a single RFC-5322 address string or an array of address strings — multi-recipient is the only field-level variation the underlying library supports without bespoke parsing.

interface SmtpMessage {
  /** Sender address (`"Name <name@example.com>"` or just the address). */
  from: string

  /** Primary recipient address(es). */
  to: string | string[]

  /** Optional CC recipient address(es). */
  cc?: string | string[]

  /** Optional BCC recipient address(es). */
  bcc?: string | string[]

  /** Subject line — plain text, no encoding required. */
  subject: string

  /** Plain-text body. At least one of `text`/`html` should be set. */
  text?: string

  /** HTML body. At least one of `text`/`html` should be set. */
  html?: string

  /** Optional `Reply-To` address. */
  replyTo?: string

  /** Optional list of attachments. */
  attachments?: SmtpAttachment[]

  /**
   * Additional headers, keyed by header name. Values that are arrays
   * produce repeated headers (e.g. multiple `Received` headers).
   */
  headers?: Record<string, string | string[]>
}

SmtpOAuth2Auth

OAuth2 bearer-token SMTP credentials (e.g. Gmail XOAUTH2).

interface SmtpOAuth2Auth {
  /** Account email / SASL identity. */
  user: string

  /** Pre-fetched OAuth2 access token. */
  accessToken: string
}

SmtpPasswordAuth

Password-style SMTP credentials.

interface SmtpPasswordAuth {
  /** SMTP username (often the full email address). */
  user: string

  /** SMTP password or app-specific password. */
  pass: string
}

Types

SmtpErrorCode

Stable machine-readable error codes emitted by {@link SmtpError}.

type SmtpErrorCode =
  | 'invalid-config'
  | 'connection-failed'
  | 'auth-failed'
  | 'tls-required'
  | 'send-failed'
  | 'timeout'
  | 'disconnected'

Classes

SmtpError

Error thrown by {@link connectSmtp} / {@link SmtpClient} methods.

code is a stable machine-readable string; message is the developer-facing English description (handler-error pattern — locale bond not required for this utility).

Functions

buildTransportOptions(config)

Build the nodemailer transport options from a {@link SmtpConfig}.

Kept as a pure function so the test suite can assert the exact shape we hand to nodemailer (and therefore confirm we never leak caller credentials beyond what is necessary).

function buildTransportOptions(config: SmtpConfig): SMTPTransport.Options
  • config — Validated config.

Returns: Options for nodemailer.createTransport.

connectSmtp(config, internals)

Connect to a user-supplied SMTP server and return a normalized {@link SmtpClient} bound to it.

The returned client wraps a single nodemailer transporter — it is safe to keep the client around for multiple sendMail calls and to share between concurrent senders.

function connectSmtp(config: SmtpConfig, internals?: ConnectSmtpInternals): Promise<SmtpClient>
  • config — SMTP connection + auth config.
  • internals — Optional test-only injection point.

Returns: Connected {@link SmtpClient}.

Injection Notes

Runtime Dependencies

  • nodemailer

Throws {@link SmtpError} (error.code is one of invalid-config, connection-failed, auth-failed, tls-required, send-failed, timeout, disconnected). Map error.code to translated user-facing text in the calling handler — this utility intentionally has no locale bond (handler-error pattern).

Network reality: this opens a raw TCP connection to the user's SMTP host (typically port 587 with STARTTLS, 465 for implicit TLS). Raw sockets do NOT traverse HTTP_PROXY-style egress proxies, so proxy-only / default-deny environments (including molecule.dev sandboxes, whose firewall permits only the HTTP(S) egress proxy) fail every connect with connection-failed or timeout even when the config is correct. Production deployments need direct outbound TCP to the target ports; note many clouds block port 25.