npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@push.rocks/smartmta

v9.5.0

Published

A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with Rust acceleration.

Readme

@push.rocks/smartmta

A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with a Rust-powered SMTP engine — no nodemailer, no shortcuts. Automatic MX record discovery means you just call sendEmail() and smartmta figures out where to deliver. 🚀

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Install

pnpm install @push.rocks/smartmta
# or
npm install @push.rocks/smartmta

After installation, run pnpm build to compile the Rust binary (mailer-bin). The Rust binary is requiredsmartmta will not start without it.

Overview

@push.rocks/smartmta is a complete mail server solution — SMTP server, SMTP client, email security, content scanning, and delivery management — all built with a custom SMTP implementation. The SMTP engine runs as a Rust binary for maximum performance, communicating with the TypeScript orchestration layer via JSON-over-stdin/stdout IPC.

⚡ What's Inside

| Module | What It Does | |---|---| | Rust SMTP Server | High-performance SMTP engine in Rust — TCP/TLS listener, STARTTLS, AUTH, pipelining, per-connection rate limiting | | Rust SMTP Client | Outbound delivery with connection pooling, retry logic, TLS negotiation, DKIM signing — all in Rust | | DKIM | Key generation, signing, and verification — per domain, with automatic rotation | | SPF | Full SPF record validation via Rust | | DMARC | Policy enforcement and verification | | Email Router | Pattern-based routing with priority, forward/process/reject actions plus terminal local deliver/store actions | | Bounce Manager | Automatic bounce detection via Rust, classification (hard/soft), and suppression tracking | | Content Scanner | Spam, phishing, malware, XSS, and suspicious link detection — powered by Rust | | IP Reputation | DNSBL checks, proxy/TOR/VPN detection, risk scoring via Rust | | Rate Limiter | Hierarchical rate limiting (global, per-domain, per-IP) | | Delivery Queue | Persistent queue with exponential backoff retry | | Template Engine | Email templates with variable substitution | | Domain Registry | Multi-domain management with per-domain configuration | | DNS Manager | Automatic DNS record management (MX, SPF, DKIM, DMARC) |

🏗️ Architecture

┌──────────────────────────────────────────────────────────────┐
│                    UnifiedEmailServer                        │
│     (orchestrates all components, emits events)              │
├───────────┬───────────┬──────────────┬───────────────────────┤
│   Email   │  Security │  Delivery    │  Configuration        │
│   Router  │  Stack    │  System      │                       │
│  ┌──────┐ │ ┌───────┐ │ ┌──────────┐ │  ┌────────────────┐   │
│  │Match │ │ │ DKIM  │ │ │ Queue    │ │  │ DomainRegistry │   │
│  │Route │ │ │ SPF   │ │ │ Rate Lim │ │  │ DnsManager     │   │
│  │ Act  │ │ │ DMARC │ │ │ Retry    │ │  │ DKIMCreator    │   │
│  └──────┘ │ │ IPRep │ │ └──────────┘ │  │ Templates      │   │
│           │ │ Scan  │ │              │  └────────────────┘   │
│           │ └───────┘ │              │                       │
├───────────┴───────────┴──────────────┴───────────────────────┤
│              Rust Security Bridge (smartrust IPC)            │
├──────────────────────────────────────────────────────────────┤
│                   Rust Acceleration Layer                    │
│  ┌──────────────┐  ┌───────────────┐  ┌──────────────────┐   │
│  │ mailer-smtp  │  │mailer-security│  │   mailer-core    │   │
│  │ SMTP Server  │  │DKIM/SPF/DMARC │  │ Types/Validation │   │
│  │ SMTP Client  │  │IP Rep/Content │  │ MIME/Bounce      │   │
│  │ TLS/AUTH     │  │   Scanning    │  │   Detection      │   │
│  └──────────────┘  └───────────────┘  └──────────────────┘   │
└──────────────────────────────────────────────────────────────┘

Data flow for inbound mail:

  1. 📨 Rust SMTP server accepts the connection and handles the SMTP protocol
  2. 🛂 If recipient validation is enabled, Rust asks TypeScript to approve each RCPT TO before DATA
  3. 🔒 On DATA completion, Rust runs the security pipeline in-process (DKIM/SPF/DMARC verification, content scanning, IP reputation check) — zero IPC round-trips
  4. 📤 Rust emits an emailReceived event via IPC with pre-computed security results attached
  5. 🔀 TypeScript accepts responsibility or rejects the message through routing and optional hooks
  6. ✅ Rust sends the final SMTP response only after TypeScript returns the acceptance decision

Data flow for outbound mail:

  1. 📝 TypeScript constructs the email and calls sendEmail() (defaults to MTA mode)
  2. 🔍 MTA mode automatically resolves MX records for each recipient domain, sorts by priority, and groups recipients for efficient delivery
  3. 🦀 Sends to Rust via IPC — Rust builds the RFC 2822 message, signs with DKIM, and delivers via its SMTP client with connection pooling
  4. 📬 Result (accepted/rejected recipients, server response) returned to TypeScript

Usage

🚀 Setting Up the Email Server

The central entry point is UnifiedEmailServer, which orchestrates the Rust SMTP server, routing, security, and delivery:

import { UnifiedEmailServer } from '@push.rocks/smartmta';

const emailServer = new UnifiedEmailServer(dcRouterRef, {
  // Ports to listen on (465 = implicit TLS by default, 25/587 = STARTTLS)
  ports: [25, 587, 465],

  // Public SMTP hostname used for greeting/banner and as the default outbound identity
  hostname: 'mail.example.com',

  // Multi-domain configuration
  domains: [
    {
      domain: 'example.com',
      dnsMode: 'external-dns',
      dkim: {
        selector: 'default',
        keySize: 2048,
        rotateKeys: true,
        rotationInterval: 90,
      },
      rateLimits: {
        outbound: { messagesPerMinute: 100 },
        inbound: { messagesPerMinute: 200, connectionsPerIp: 20 },
      },
    },
  ],

  // Routing rules (evaluated by priority, highest first)
  routes: [
    {
      name: 'catch-all-forward',
      priority: 10,
      match: {
        recipients: '*@example.com',
      },
      action: {
        type: 'forward',
        forward: {
          host: 'internal-mail.example.com',
          port: 25,
        },
      },
    },
    {
      name: 'reject-spam-senders',
      priority: 100,
      match: {
        senders: '*@spamdomain.com',
      },
      action: {
        type: 'reject',
        reject: {
          code: 550,
          message: 'Sender rejected by policy',
        },
      },
    },
    {
      name: 'authenticated-outbound-relay',
      priority: 90,
      match: {
        authenticated: true,
        recipients: '*@*',
      },
      action: {
        type: 'process',
        // External relay is denied by default. Set allowRelay only on routes that explicitly permit it.
        allowRelay: true,
      },
    },
  ],

  smtp: {
    // Enabled by default: ask TypeScript to approve every RCPT TO before DATA.
    recipientValidation: true,
    // Listener port that uses implicit TLS. Defaults to 465 when present in ports.
    // Set this for remapped backend listeners, e.g. 10465 behind a proxy.
    securePort: 465,
    recipientValidationTimeoutMs: 5000,

    // Timeout for hooks.onMessageData and hooks.onAcceptEnvelope before
    // returning a temporary SMTP failure.
    messageAcceptanceTimeoutMs: 30000,

    // Set required to true only on backend listeners reached through trusted proxies.
    proxyProtocol: {
      required: false,
      trustedIps: [],
    },
  },

  hooks: {
    async onRcptTo(context) {
      if (context.abortSignal?.aborted) {
        return { accepted: false, smtpCode: 451, smtpMessage: 'Recipient policy timeout' };
      }
      const recipientDomain = context.rcptTo.split('@').pop()?.toLowerCase();
      if (recipientDomain === 'example.com') {
        return { accepted: true };
      }
      if (context.authenticated) {
        // External relay still requires a matched route with action.allowRelay: true.
        return { accepted: true };
      }
      return { accepted: false, smtpCode: 550, smtpMessage: 'Relay not permitted' };
    },
    async onMessageData(context) {
      // Runs for envelopes WITHOUT a resolved store/deliver action.
      // Return accepted only after the consumer has taken responsibility for the message.
      // await durableStore.save(context.email, context.rawMessage, { signal: context.abortSignal });
      return { accepted: true, continueProcessing: false };
    },
    async onAcceptEnvelope(context) {
      // Required terminal durable owner for envelopes with a resolved
      // `store` or `deliver` action. Durably persist the EXACT raw bytes and
      // the per-recipient plan before resolving — the SMTP 250 is only sent
      // after this hook resolves.
      // const plan = emailServer.createAcceptedEnvelopeDispatchPlan(context, idempotencyKeys);
      // await durableStore.saveEnvelope(context.rawMessage, plan, { signal: context.abortSignal });
    },
  },

  // Authentication settings for the SMTP server.
  // Legacy plaintext `users` and hashed `accounts` coexist; accounts win on
  // username collision. See "Authenticated Accounts And Send Scopes" below.
  auth: {
    required: false,
    methods: ['PLAIN', 'LOGIN'],
    users: [{ username: 'outbound', password: 'secret' }],
    accounts: [{
      username: 'newsletter-service',
      credential: { verifier: storedScramVerifier },
      scope: {
        senders: ['[email protected]'],
        recipients: { mode: 'any' },
      },
    }],
  },

  // TLS material is provided in memory; SmartMTA does not read certificate files.
  tls: {
    certPem: await certificateStore.getCertificatePem('mail.example.com'),
    keyPem: await certificateStore.getPrivateKeyPem('mail.example.com'),
  },

  outbound: {
    // Optional override for outbound EHLO/HELO identity
    hostname: 'smtp-out.example.com',
    // Optional physical connection proxy for one-shot edge egress tunnels.
    // The logical SMTP host remains the MX/target host for TLS identity.
    connectionProxyProvider: async ({ host, port, ehloDomain }) => null,
  },

  // Durable message bodies belong in SmartBucket (or another managed blob store).
  // Use { storageMode: 'memory' } only for deliberately non-durable runtimes.
  queue: {
    storageMode: 'managed',
    storageManager: smartBucketBlobStorage,
  },

  maxMessageSize: 25 * 1024 * 1024, // 25 MB
  maxClients: 500,
});

// start() boots the Rust SMTP server, security bridge, DNS records, and delivery queue
await emailServer.start();

🔒 Note: start() will throw if the Rust binary is not compiled. Run pnpm build first.

SmartMTA requires Node.js 22 or newer.

hostname is the public SMTP identity for greetings and outbound delivery by default. It is not a bind address.

dcRouterRef.storageManager implements IStorageManager for small textual state such as DKIM keys, routes, bounce metadata, and reputation cache; use SmartData for that adapter. Queue bodies and attachment blobs use IBlobStorageManager, normally backed by SmartBucket. SmartMTA has no local-filesystem persistence fallback.

Durable Delivery Checkpoints

Managed queues store complete queue items as SmartBucket blobs. Schema V2 adds per-recipient delivery states and a durable bounce outbox:

  • A recipient becomes delivered only after the remote SMTP server's final 250 response.
  • A permanent 5xx RCPT TO response atomically stores both the recipient's terminal state and its pending bounce operation in one queue-item blob mutation.
  • A temporary 4xx response remains unresolved, so only that recipient is attempted against the next MX host or a later queue retry. Delivered and permanently rejected recipients are never resent.
  • A newly committed item wakes the normal eligibility scan immediately. Calling resume() also immediately wakes eligible queued work. The periodic scan remains active for retry deadlines and recovery, while paused queues do not dispatch until resumed.
  • IMultiModeDeliveryOptions.globalRateLimit counts eligible delivery items in a fixed one-minute window. Each item admitted for a delivery claim consumes one slot; capacity and rate-window releases wake remaining work without waiting for the periodic scan.
  • Bounce operation IDs are deterministic hashes of the queue item, normalized recipient, and operation kind. Pending work is drained immediately at delivery-system startup and periodically afterward, including work on terminal queue items.
  • Observer and event-listener failures are logged and isolated after the durable state transition; they cannot reclassify delivery state or statistics.

V1 queue blobs remain readable. They hydrate with an empty bounce outbox and are upgraded to V2 on their next mutation; SmartMTA never invents retroactive bounce work. V2 blobs are intentionally not readable by V7, so rolling back after a V2 write is unsupported.

The managed-prefix ownership guard is process-local only. It coordinates queues only when they share the exact same IBlobStorageManager object instance and normalized storage prefix. Different storage-wrapper instances and different JavaScript processes are not coordinated; the embedder must enforce one delivery owner or provide an external distributed lease. This guard does not provide distributed ownership. Structured mutations remain forward-only to schema V2, while raw items use schema V3; older releases cannot read records from newer schemas.

Bounce delivery is at-least-once. BounceManager uses the operation ID as a durable processed marker and deduplicates concurrent/restarted calls. Its in-process completed-operation LRU is only a hot-duplicate optimization, bounded to 10,000 entries with a 24-hour TTL; cache misses always re-read the durable marker. A process crash after an external side effect but before the queue completion checkpoint can cause the operation to be presented again. Custom bounceHandler.processSmtpFailure implementations receive options.operationId and must deduplicate it in their own durable store; SmartMTA does not claim exactly-once execution for custom handlers.

Durable Raw Outbound Queue

enqueueRawEmail() is the narrow exact-byte path for already-constructed RFC822 messages that still need SmartMTA's durable MTA delivery. It always uses MTA mode, MX discovery, self-MX protection, per-recipient checkpoints, retries, and bounce handling. Callers cannot select a route, processing mode, transformation, or bypass.

const queueId = await emailServer.enqueueRawEmail(
  exactRfc822Buffer,
  {
    from: '[email protected]',
    to: ['[email protected]', '[email protected]'],
  },
  'stable-upstream-message-id',
);
  • The message must be a nonempty Buffer with canonical CRLF line endings and a final CRLF. SmartMTA rejects bare CR, bare LF, and missing final CRLF so SMTP DATA framing cannot change the signed message bytes. It stores the exact unsigned bytes without parsing the body or attachments. queue.maxRawMessageBytes defaults to 32 MiB.
  • The SMTP envelope is separate from RFC822 headers. Sender and recipients are canonicalized, recipients must be unique after canonicalization, and caller order is preserved. Header/envelope divergence is intentional; delivery recipients always come from the SMTP envelope.
  • The idempotency fingerprint is versioned and length-framed over the exact bytes, canonical sender, ordered canonical recipients, and fixed raw-MTA/mandatory-DKIM semantics. Reusing a key with different bytes, sender, recipients, or recipient order is a conflict.
  • Raw enqueue requires queue.storageMode: 'managed', an IBlobStorageManager, and backend-bounded listPage(). Managed startup hydration uses bounded pages whenever the backend provides them. Raw records use schema V3 and keep metadata plus the exact Buffer in one atomic queue blob; V1/V2 structured records and their hydration remain unchanged.
  • Queue memory contains only a tiny metadata Email (sender, recipients, empty subject/text, no attachments), raw SHA-256/length, and a cloned SMTP envelope. Public queue getters, events, and server status getters never expose raw bytes or live queue records.
  • Delivery rereads and verifies one V3 blob only for the duration of a callback. queue.rawInFlightByteBudget defaults to 192 MiB and must be at least six times maxRawMessageBytes. Raw enqueue is charged at three times the message length; payload reread, persistence, direct raw send, base64, and IPC work is charged at six times the message length. Enqueue and direct-send admission fail immediately when the byte budget is occupied or the hard limit of 1,024 concurrent raw reservations is reached, rather than retaining unbounded caller buffers or promises. Shutdown closes reservation admission and drains accepted reservations before clearing queue state. This is a conservative concurrency budget for SmartMTA-owned transient representations, not a hard process-RSS limit. No payload cache or separate payload blobs exist.
  • DKIM is mandatory and fail-closed. At every delivery attempt SmartMTA resolves the sender domain's current selector from DomainRegistry, loads the matching current key, and asks Rust to prepend the signature directly to the decoded bytes. Missing keys, invalid keys, or signing failures send nothing. The unsigned queue bytes remain unchanged across MX attempts and retries.
  • sendRawOutboundEmail() is the corresponding low-level direct-target transport. It requires the caller to provide an exact dkimSelector for the envelope sender's dkimDomain; unlike queued delivery, it does not resolve a current selector from DomainRegistry. It uses the same connection proxy, source evidence, authentication capture, timeouts, TLS/opportunistic TLS, pooling, and cleanup path as sendOutboundEmail().

Schema V3 is a downgrade gate. SmartMTA 9.4 cannot read V3. Downgrade to 9.4 is safe only after every raw item has drained or been explicitly removed and a current getQueueItems() inventory proves there are zero items with messageFormat === 'raw-rfc822'. Do not infer this from enqueue traffic or stale metrics.

The single-writer prerequisite is strict for raw and structured queue records alike. The process-local managed-prefix guard is not a distributed lease; deployments with multiple processes or storage-wrapper instances must provide an external lease so exactly one delivery owner mutates a queue prefix.

🛂 Inbound Recipient Policy And Relay Control

SmartMTA validates each RCPT TO before the client can enter DATA by default. The default policy evaluates routes and accepts configured local domains. External recipients are rejected unless the matched route action explicitly sets allowRelay: true. Set smtp.recipientValidation: false only on fully trusted private listeners where another layer performs recipient and relay policy.

Use allowRelay only for intentional relay routes, for example authenticated submission or tightly scoped internal networks. Broad process routes do not grant relay permission by themselves, and deliver routes can never relay — allowRelay is ignored for them.

Submission-permission routes never claim a locally terminated recipient

A submission route grants an authenticated client permission to relay. It does not turn a local delivery into a remote one. A matched route is therefore ignored for a given recipient, and resolution falls through to that domain's inbound policy, when all of the following hold:

  • the recipient domain is registered and its inboundPolicy.defaultAction is store, so this server really is the destination;
  • the action is process — the only action whose transport is DNS MX relay;
  • the route names no recipient scope: match.recipients is absent, empty, or consists only of match-all patterns (*, *@*).

Without this, an authenticated submission addressed to a domain this server hosts is accepted with 250, relayed to that domain's MX, and destroyed by the self-MX loop guard. Everything else keeps today's behaviour: recipient-scoped process routes still win (consumers use them for local addresses handled by their own acceptance pipeline), store/deliver/forward/reject actions always win, and a domain registered for outbound identity only — no inboundPolicy, or the send-only defaultAction: 'reject' — keeps relaying because its real MX lives elsewhere. Encoding recipient intent in match.headers is not recognised for this purpose.

hooks.onRcptTo, hooks.onMessageData, and hooks.onAcceptEnvelope are awaited asynchronously. If a hook throws or times out, SmartMTA returns a temporary SMTP failure. The hook context includes abortSignal; long-running consumers should observe it and stop work promptly. A custom onRcptTo hook may accept local recipients using application-specific lookup, but external relay is still blocked unless a matched route explicitly sets allowRelay: true.

hooks.onMessageData runs after DATA and before the final SMTP response — but only for envelopes with no resolved store or deliver action; terminal local deliveries go through hooks.onAcceptEnvelope instead and never invoke onMessageData. Return { accepted: true } only after your application has accepted responsibility for the message, such as after durable persistence. Set continueProcessing: false when the consumer owns all downstream processing.

hooks.onAcceptEnvelope is the terminal durable owner for any envelope containing a resolved store or deliver action. It receives the exact raw RFC822 bytes (rawMessage), the complete per-recipient route resolution (resolvedRecipientRoutes), and the Rust security verdicts (securityResults). It must durably persist the message before resolving — the SMTP 250 is only sent after the hook resolves. Throwing an AcceptEnvelopeRejectionError with a 5xx code rejects the envelope permanently with that status; any other error or a smtp.messageAcceptanceTimeoutMs timeout refuses the envelope with a temporary 451. Use createAcceptedEnvelopeDispatchPlan() inside the hook to snapshot the resolved plan for later replay-safe dispatchAcceptedEnvelope() calls. Both AcceptEnvelopeRejectionError and IAcceptEnvelopeContext are exported.

Use smtp.securePort when the implicit TLS listener uses a remapped backend port instead of public 465; the configured secure port must also be present in ports, otherwise start() throws.

Trusted PROXY protocol v1 support is configured with smtp.proxyProtocol. Only enable required: true on backend listeners that are reached through a trusted local or private proxy, and always provide trustedIps. Required PROXY mode fails closed when no trusted peers are configured.

🔒 AUTH Is Only Offered On An Encrypted Transport

AUTH is never advertised in the EHLO capabilities of a cleartext session, and an AUTH command on a cleartext session is refused with 538 5.7.11 Encryption required for requested authentication mechanism — so credentials are never solicited or accepted over an unencrypted channel, even by a client that ignores the missing capability. This applies to PLAIN, LOGIN and SCRAM-SHA-256 alike. A cleartext listener with TLS material still advertises STARTTLS, so clients upgrade first and then see AUTH.

A transport counts as encrypted when this server terminated TLS (implicit TLS on smtp.securePort, or after a successful STARTTLS) or when the port is declared in smtp.tlsTerminatedPorts:

smtp: {
  // Public 465 is terminated by the edge proxy, which forwards the decrypted
  // stream to this backend port. The transport is encrypted end-to-client even
  // though this process performed no handshake.
  tlsTerminatedPorts: [10465],
  proxyProtocol: { required: true, trustedIps: ['127.0.0.1', '::1'] },
}

That is a trust assertion about the peer, so it is only honoured together with proxyProtocol.required and a non-empty trustedIps — the exact-peer PROXY check is what proves the connection really came from the terminator. start() throws when a listed port is not one of the configured ports, is the implicit-TLS securePort, or PROXY protocol is not required. Sessions on such a port report secure: true to hooks and do not advertise STARTTLS.

updateOptions restarts only the Rust SMTP listener when the AUTH presence transitions, when tls.certPem/tls.keyPem are replaced (certificate renewal), or when smtp.tlsTerminatedPorts changes — those are listener-level Rust configuration and would otherwise be merged into the options and never take effect.

⚠️ Changed in v9.3: listeners that previously advertised and accepted AUTH in cleartext no longer do. If TLS material never reaches tls.certPem/tls.keyPem, a plain port offers neither STARTTLS nor AUTH — verify that submission ports actually receive certificate material, or declare them in smtp.tlsTerminatedPorts when an upstream proxy terminates TLS.

Note that tls.caPem, tls.minVersion and tls.ciphers are currently accepted but not consumed by the Rust listener; only certPem and keyPem take effect.

🔐 Authenticated Accounts And Send Scopes

auth.accounts defines authenticated SMTP submission accounts with hashed credentials and per-account send authorization. It coexists with the legacy plaintext auth.users array; when both define the same username, the account wins.

import type { ISmtpAuthAccount } from '@push.rocks/smartmta';

const accounts: ISmtpAuthAccount[] = [
  {
    username: 'newsletter-service',
    // Hashed SCRAM-SHA-256 verifier — the plaintext password is never stored.
    // SaltedPassword = PBKDF2-HMAC-SHA256(password, salt, iterations, 32)
    // StoredKey = SHA256(HMAC-SHA256(SaltedPassword, 'Client Key'))
    // ServerKey = HMAC-SHA256(SaltedPassword, 'Server Key')
    credential: {
      verifier: {
        algorithm: 'SCRAM-SHA-256',
        salt: '<base64 salt>',
        iterations: 4096, // minimum
        storedKey: '<base64 StoredKey>',
        serverKey: '<base64 ServerKey>',
      },
    },
    scope: {
      // "send AS who": enforced against the SMTP MAIL FROM envelope sender
      // AND the parsed message header From address.
      senders: ['[email protected]', '*@mail.example.com'],
      // "send TO who": optional; absent or mode 'any' = unrestricted.
      recipients: { mode: 'patterns', patterns: ['*@example.com'] },
    },
  },
  {
    username: 'trusted-app',
    // Plaintext credential is supported for migration, but verifiers are preferred.
    credential: { password: 'app-password' },
    // No scope: unrestricted, exactly like a legacy auth.users entry.
  },
];

server.updateOptions({ auth: { accounts } });

Enforcement semantics:

  • CredentialsPLAIN/LOGIN passwords are verified against the stored verifier by deriving StoredKey from the presented password (PBKDF2-HMAC-SHA256 on the async crypto threadpool) and comparing in constant time. SCRAM-SHA-256 clients are served the stored verifier fields directly, so the plaintext never needs to exist server-side. Accounts with enabled: false always fail authentication on both mechanisms. Verification is fully in-memory — no I/O inside the Rust auth callback window.
  • Sender scope ("send AS who") — enforced twice. At the first RCPT TO, an out-of-scope MAIL FROM is refused with 553 before any recipient is accepted (with smtp.recipientValidation: false this check still runs, but only at DATA). At DATA, the envelope sender (taken from session envelope truth, never the parsed message alone) is re-checked per recipient, and every parsed header From mailbox must independently be in scope — header-From spoofing behind an in-scope envelope, including multi-mailbox From: headers, is refused with 553.
  • Recipient scope ("send TO who") — with recipients.mode: 'patterns', every RCPT TO matching no pattern is refused with 550. mode: 'any' or an absent recipients block leaves recipients unrestricted.
  • Patterns are exact addresses ([email protected]) or glob patterns (*@b.com), matched case-insensitively with identical semantics to route matching. A scoped account sending with an empty MAIL FROM <> (null sender) is refused unless a pattern explicitly matches — senders: ['*'] is the documented escape hatch.
  • Route binding — a route with match.authenticatedUser: 'newsletter-service' (or an array of usernames) matches only sessions authenticated as that account (exact, case-sensitive username comparison), so dedicated processing or relay routes can be attached per account. Unauthenticated sessions never match such routes.
  • Rejections surface through onRecipientRejected with the reasons sender-scope, recipient-scope, and account-disabled.
  • Live enablement — when a configuration update transitions between zero and one-or-more accounts/users, updateOptions restarts only the Rust SMTP listener so the AUTH capability appears or disappears without a process restart. A failed listener restart logs loudly, emits smtpListenerRestartFailed, and leaves isSmtpListenerActive() false — surface that next to getBridgeState() in health endpoints.
  • An account without a scope is authenticated but unrestricted — identical to legacy auth.users behavior, which itself is unchanged.

Configuration is validated loudly at construction and updateOptions time: unusable accounts (empty scope.senders, mode: 'patterns' without patterns, both or neither credential kind, malformed base64 keys, iterations outside [4096, 1000000], duplicate usernames) throw instead of degrading into silent deny-all or silent auth failure. Note that updateOptions({ auth }) replaces the whole auth block — include users, required, and methods again if they should persist.

Deriving verifiers: deriveScramVerifier(password, iterations?) is the canonical embedder-facing derivation, matching exactly what the server verifies at AUTH time — persist only the returned verifier. The exported SCRAM_DEFAULT_ITERATIONS (600,000) is OWASP-aligned to protect human-chosen passwords against offline attack on a leaked verifier store, at roughly 100-150ms per derivation and per PLAIN/LOGIN authentication on the bounded async pool. Machine-generated high-entropy secrets gain nothing from high counts — callers may pass any lower value within [SCRAM_MIN_ITERATIONS, SCRAM_MAX_ITERATIONS]. assertValidAuthAccounts is exported too, so embedders can pre-validate database-sourced account sets with the exact rules the server enforces.

Compatibility probe: smartMtaCapabilities.smtpAuthAccountsAndScopes.

📧 Sending Emails (Automatic MX Discovery)

The recommended way to send email is sendEmail(). It defaults to MTA mode, which automatically resolves MX records for each recipient domain via DNS — you don't need to know the destination mail server:

import { Email, UnifiedEmailServer } from '@push.rocks/smartmta';

// Build an email
const email = new Email({
  from: '[email protected]',
  to: ['[email protected]', '[email protected]'],
  subject: 'Hello from smartmta! 🚀',
  text: 'Plain text body',
  html: '<h1>Hello!</h1><p>HTML body with <strong>formatting</strong></p>',
  priority: 'high',
  attachments: [
    {
      filename: 'report.pdf',
      content: pdfBuffer,
      contentType: 'application/pdf',
    },
  ],
});

// Send — MTA mode auto-discovers MX servers for gmail.com and company.org
const emailId = await emailServer.sendEmail(email);

// Optionally specify a delivery mode explicitly
const emailId2 = await emailServer.sendEmail(email, 'mta');

sendEmail() returns the delivery queue item ID, which you can later use with queue/status APIs.

In MTA mode, smartmta:

  • 🔍 Resolves MX records for each recipient domain (e.g. gmail.com, company.org)
  • 📊 Sorts MX hosts by priority (lowest = highest priority per RFC 5321)
  • 🔄 Tries each MX host in order until delivery succeeds
  • 🌐 Falls back to the domain's A record if no MX records exist
  • 📦 Groups recipients by domain for efficient batch delivery
  • 🔑 Signs outbound mail with DKIM automatically

📮 Delivery Modes

sendEmail() accepts a mode parameter that controls how the email is delivered:

public async sendEmail(
  email: Email,
  mode: EmailProcessingMode = 'mta',  // 'mta' | 'forward' | 'process'
  route?: IEmailRoute,
  options?: {
    skipSuppressionCheck?: boolean;
    ipAddress?: string;
    isTransactional?: boolean;
  }
): Promise<string>

| Mode | Description | |---|---| | mta (default) | Auto MX discovery — resolves MX records via DNS, delivers directly to the recipient's mail server. No relay configuration needed. | | forward | Relay delivery — forwards the email to a configured SMTP host (e.g. an internal mail gateway or third-party relay). | | process | Scan + deliver — runs the content scanning / security pipeline first, then delivers via auto MX resolution. |

Self-MX loop guard: MTA delivery refuses any MX host that equals the server's own hostname (hostname or outbound.hostname). A domain whose MX record points back at this server would otherwise re-enter the inbound pipeline on every delivery attempt — an infinite mail loop. Such deliveries fail with MX for <domain> points back at this server — refusing self-delivery to avoid a mail loop. The comparison is hostname-based; if a target only reveals the loop at the IP level (e.g. an A-record fallback), embedders should add an IP-level guard in their connectionProxyProvider.

Routes with action deliver never enter MTA mode: they are terminal local deliveries through the durable acceptance path (see Route Action Types below), so the self-MX guard is structurally unreachable for them. Pre-9.x persisted queue items that still carry a deliver action fail loudly at delivery time with error type deliver_action_relay_unsupported (failure class localTopology) instead of being relayed or bounced as a misleading self-MX loop.

📬 Direct SMTP Delivery (Low-Level)

For cases where you know the exact target SMTP server (e.g. relaying to a specific host), use the lower-level sendOutboundEmail():

// Send directly to a known SMTP server (bypasses MX resolution)
const result = await emailServer.sendOutboundEmail('smtp.example.com', 587, email, {
  auth: { user: '[email protected]', pass: 'your-password' },
  dkimDomain: 'example.com',
  dkimSelector: 'default',
});

console.log(`Accepted: ${result.accepted.join(', ')}`);
console.log(`Response: ${result.response}`);
if (result.outboundAuthenticationHandle) {
  console.log(result.outboundAuthenticationHandle.pending);
  console.log(await result.outboundAuthenticationHandle.result);
}
// -> Accepted: [email protected]
// -> Response: 2.0.0 Ok: queued

The sendOutboundEmail method:

  • 🔑 Automatically resolves DKIM keys from the DKIMCreator for the specified domain and exact dkimSelector (defaulting to default), so the loaded private key always matches the selector written into the signature
  • 🔗 Uses connection pooling in Rust for direct sends — reuses TCP/TLS connections across sends
  • ⏱️ Configurable connection and socket timeouts via outbound options on the server
  • 🪪 Uses outbound.hostname as the SMTP identity when configured, otherwise falls back to hostname; a connection proxy's ehloHostname overrides both for that connection

sendRawOutboundEmail(host, port, rawMessage, envelope, { dkimDomain, dkimSelector }) provides the same transport behavior for exact bytes. It accepts only canonical CRLF-terminated message bytes and a canonical separate SMTP envelope, requires dkimDomain to equal the envelope sender domain, and loads the exact caller-supplied selector. It fails before SMTP if that selector/key cannot be loaded or Rust cannot sign the decoded bytes.

When a connection proxy returns sourceEvidence, successful results and SmtpDeliveryError instances carry a non-enumerable outboundAuthenticationHandle. Its pending value is immediately safe to persist; its result promise evaluates SPF, DKIM, and DMARC over the exact post-DKIM message bytes and the actual connected source address. Missing source evidence, capture limits, DNS failures, and timeouts become indeterminate and never change the SMTP delivery outcome. Captures are one-shot, memory-only, size-bounded, and expire after two minutes. A capture remains charged against the global capture budget until its active verification finishes.

For deployments that need outbound traffic to leave through a separate edge, configure outbound.connectionProxyProvider. SmartMTA keeps the logical host/port as the SMTP target for MX identity and TLS/SNI, but dials the returned connectHost/connectPort as the physical TCP endpoint. Proxied sends automatically disable Rust connection pooling and call close() in a finally block, which is suitable for one-shot local proxies. The provider context also includes senderDomain (lowercased domain of the sending address, undefined when unparsable) so providers can route by sender, and the returned proxy may set ehloHostname to override the EHLO/HELO identity for that connection — e.g. the selected egress edge's PTR hostname.

const emailServer = new UnifiedEmailServer(dcRouter, {
  hostname: 'mx.example.com',
  outbound: {
    hostname: 'mx.example.com',
    connectionProxyProvider: async (context) => {
      const proxy = await remoteIngressHub.startEgressTcpProxy({
        edgeId: 'edge-fra-01',
        logicalHost: context.host,
        port: context.port,
        serverFirst: true,
      });

      return {
        connectHost: proxy.listenHost,
        connectPort: proxy.listenPort,
        poolKey: proxy.proxyId,
        sourceEvidence: remoteIngressHub.waitForEgressConnectionEvidence(
          proxy.proxyId,
          proxy.edgeId,
        ),
        close: () => remoteIngressHub.stopEgressTcpProxy(proxy.proxyId),
      };
    },
  },
});

🔑 DKIM Signing & Key Management

DKIM key management is handled by DKIMCreator, which generates, stores, and rotates keys per domain. Signing is performed automatically by the Rust SMTP client during outbound delivery:

import { DKIMCreator } from '@push.rocks/smartmta';

// Small structured key state is supplied by the embedder through managed storage.
// Use a SmartData-backed IStorageManager; SmartMTA never writes keys to local files.
const dkimCreator = new DKIMCreator(storageManager);

// Auto-generate keys if they don't exist
await dkimCreator.handleDKIMKeysForDomain('example.com');

// Get the DNS record you need to publish
const dnsRecord = await dkimCreator.getDNSRecordForDomain('example.com');
console.log(dnsRecord);
// -> { type: 'TXT', name: 'default._domainkey.example.com', value: 'v=DKIM1; k=rsa; p=...' }

// Check if keys need rotation
const needsRotation = await dkimCreator.needsRotation('example.com', 'default', 90);
if (needsRotation) {
  const newSelector = await dkimCreator.rotateDkimKeys('example.com', 'default', 2048);
  console.log(`Rotated to selector: ${newSelector}`);
}

When UnifiedEmailServer.start() is called:

  • In the default automatic mode, DKIM keys are generated or loaded for every configured domain and rotation follows rotationInterval.
  • With dkimKeyProvisioning: 'caller-managed', SmartMTA only loads the requested selector from managed storage; it never generates or rotates keys.
  • A requested missing or invalid key fails closed before SMTP delivery. SmartMTA never falls back to unsigned delivery.
  • Signing is applied exactly once, by the Rust SMTP client at final outbound transmission.
  • When a route enables signing without explicit dkimOptions, the selector is resolved from the sending domain's registry configuration (domains[].dkim.selector); the hardcoded 'default' selector is only the last fallback.

Embedders can check smartMtaCapabilities.selectorCorrectDkimSigning, callerManagedDkimKeys, structuredSmtpTransactions, typedDeliveryFailures, terminalLocalDeliverActions (deliver routes are terminal local deliveries through the durable acceptance path), smtpAuthAccountsAndScopes (authenticated accounts with hashed credentials and send scopes), and durableRawOutboundQueue as compatibility probes.

🛡️ Email Authentication (SPF, DKIM, DMARC)

All verification is powered by the Rust binary. For inbound mail, UnifiedEmailServer runs the full security pipeline automatically — DKIM, SPF, DMARC, content scanning, and IP reputation in a single Rust pass. Results are attached as headers (Received-SPF, X-DKIM-Result, X-DMARC-Result).

For direct TypeScript usage, SpfVerifier is exported as a lightweight SPF record parser. DKIM signing/key management is handled through DKIMCreator and the Rust bridge; SPF, DKIM, and DMARC verification results are produced by the automatic inbound security pipeline.

import { SpfVerifier } from '@push.rocks/smartmta';

// Local SPF record parsing
const spfVerifier = new SpfVerifier();
const spfRecord = spfVerifier.parseSpfRecord('v=spf1 ip4:192.0.2.0/24 -all');
// -> { version: 'spf1', mechanisms: [...], modifiers: {} }

🔀 Email Routing

Pattern-based routing engine with priority ordering and flexible match criteria. Routes are evaluated by priority (highest first):

import { EmailRouter } from '@push.rocks/smartmta';

const router = new EmailRouter([
  {
    name: 'admin-mail',
    priority: 100,
    match: {
      recipients: '[email protected]',
      authenticated: true,
    },
    action: {
      // Terminal local delivery. In UnifiedEmailServer this requires a
      // configured hooks.onAcceptEnvelope durable local-delivery owner.
      type: 'deliver',
    },
  },
  {
    name: 'external-forward',
    priority: 50,
    match: {
      recipients: '*@example.com',
      sizeRange: { max: 10 * 1024 * 1024 }, // under 10MB
    },
    action: {
      type: 'forward',
      forward: {
        host: 'backend-mail.internal',
        port: 25,
        preserveHeaders: true,
      },
    },
  },
  {
    name: 'process-with-scanning',
    priority: 10,
    match: {
      recipients: '*@*',
    },
    action: {
      type: 'process',
      process: {
        scan: true,
        dkim: true,
        queue: 'normal',
      },
    },
  },
]);

// Evaluate routes against an email context
const matchedRoute = await router.evaluateRoutes(emailContext);

Route Action Types

| Action | Description | |---|---| | forward | Forward the email to another SMTP server via the Rust SMTP client | | deliver | Terminal local delivery. The envelope is durably accepted through hooks.onAcceptEnvelope before the SMTP 250; nothing is enqueued for outbound MX relay. | | store | Terminal local delivery like deliver, with explicit retention configuration (store.retentionDays) | | process | Queue for processing (with optional content scanning and DKIM signing), then deliver via automatic MX resolution | | reject | Reject with a configurable SMTP error code and message |

Local delivery: deliver and store

deliver and store are the two terminal local-delivery actions. An envelope that resolves to either goes through the durable acceptance path: SmartMTA calls hooks.onAcceptEnvelope with the exact raw message and the complete per-recipient plan, and only sends the SMTP 250 after the hook resolves — the consumer's durably persisted copy IS the delivery. In dispatchAcceptedEnvelope, both report status: 'stored' with no queueId; the actionType field distinguishes them.

  • A deliver route requires a configured hooks.onAcceptEnvelope. Without it the configuration is unservable and is rejected loudly whenever routes are supplied or loaded: the constructor, updateEmailRoutes, updateOptions calls that include routes, and persisted-route hydration all throw. If hook state is nevertheless broken at runtime (for example an updateOptions call that replaces hooks without passing routes), deliver recipients are refused with a temporary 451 Local delivery temporarily unavailable at RCPT TO/DATA. Mail is never accepted and then destroyed.
  • deliver never relays. Non-local recipients matched by a deliver route are refused with 550 at RCPT TO, and allowRelay is ignored for deliver actions.
  • deliver envelopes skip hooks.onMessageData, bounce-notification detection, and the inbound processEmailByMode pipeline — exactly like store envelopes. Rust-computed security verdicts are still delivered in the acceptance context.
  • deliver carries no retention metadata; the consumer owns retention for delivered messages. Use store with store.retentionDays when retention must be explicit.

⚠️ Changed in v9: before v9, deliver silently meant "relay via MX discovery". On a domain whose MX points back at this server, that accepted mail with 250 and then terminally bounced it on the self-MX loop guard — destroying mail. deliver now delivers locally, and configurations that cannot do so fail at config/accept time.

Every action can include allowRelay?: boolean. It defaults to false. Non-local recipients are accepted only when the matched action explicitly sets allowRelay: true; configured local domains remain eligible for inbound delivery without relay permission. allowRelay has no effect on deliver actions.

Match Criteria

| Criterion | Description | |---|---| | recipients | Glob patterns for recipient addresses (*@example.com) | | senders | Glob patterns for sender addresses | | clientIp | IP addresses or CIDR ranges | | authenticated | Require authentication status | | authenticatedUser | Require the session to be authenticated as this exact username (string or array of usernames) | | headers | Match specific headers (string or RegExp) | | sizeRange | Message size constraints ({ min?, max? }) | | subject | Subject line pattern (string or RegExp) | | hasAttachments | Filter by attachment presence |

⏱️ Rate Limiting

Hierarchical rate limiting to protect your server and maintain deliverability:

import { Delivery } from '@push.rocks/smartmta';
const { UnifiedRateLimiter } = Delivery;

const rateLimiter = new UnifiedRateLimiter({
  global: {
    maxMessagesPerMinute: 1000,
    maxRecipientsPerMessage: 500,
    maxConnectionsPerIP: 20,
    maxErrorsPerIP: 10,
    maxAuthFailuresPerIP: 5,
    blockDuration: 600000, // 10 minutes
  },
  domains: {
    'example.com': {
      maxMessagesPerMinute: 100,
      maxRecipientsPerMessage: 50,
    },
  },
});

// Check before sending
const allowed = rateLimiter.checkMessageLimit(
  '[email protected]',
  '192.168.1.1',
  recipientCount,
  undefined,
  'example.com'
);

if (!allowed.allowed) {
  console.log(`Rate limited: ${allowed.reason}`);
}

📬 Bounce Management

Automatic bounce detection (via Rust), classification, and suppression tracking:

import { Core } from '@push.rocks/smartmta';
const { BounceManager } = Core;

const bounceManager = new BounceManager({ storageManager: smartDataStorage });

// Process an SMTP failure
const bounce = await bounceManager.processSmtpFailure(
  '[email protected]',
  '550 5.1.1 User unknown',
  {
    operationId: 'queue-item-and-recipient-derived-operation-id',
    originalEmailId: 'msg-123',
  }
);
// -> { bounceType: 'invalid_recipient', bounceCategory: 'hard', ... }

// Check if an address is suppressed due to bounces
const suppressed = bounceManager.isEmailSuppressed('[email protected]');

// Manage the suppression list
bounceManager.addToSuppressionList('[email protected]', 'repeated hard bounces');
bounceManager.removeFromSuppressionList('[email protected]');

📝 Email Templates

Template engine with variable substitution for transactional and notification emails:

import { Core } from '@push.rocks/smartmta';
const { TemplateManager } = Core;

const templates = new TemplateManager({
  from: '[email protected]',
  footerHtml: '<p>&copy; 2026 Example Corp</p>',
});

// Register a template
templates.registerTemplate({
  id: 'welcome',
  name: 'Welcome Email',
  description: 'Sent to new users',
  from: '[email protected]',
  subject: 'Welcome, {{name}}!',
  bodyHtml: '<h1>Welcome, {{name}}!</h1><p>Your account is ready.</p>',
  bodyText: 'Welcome, {{name}}! Your account is ready.',
  category: 'transactional',
});

// Create an Email object from the template
const email = await templates.createEmail('welcome', {
  to: '[email protected]',
  variables: { name: 'Alice' },
});

🌍 DNS Management

DNS behavior is explicit per domain and DKIM ownership mode:

const emailServer = new UnifiedEmailServer(dcRouterRef, {
  hostname: 'mail.example.com',
  domains: [
    {
      domain: 'example.com',
      dnsMode: 'external-dns', // managed via Cloudflare API
    },
  ],
  // ... other config
});

// In automatic key-provisioning mode, internal DNS domains register the
// configured MX/SPF/DMARC handlers and the generated selector-specific DKIM record.
// External DNS domains are validated and report the required public changes.
await emailServer.start();

// With dkimKeyProvisioning: 'caller-managed', startup only validates the exact
// configured selector in managed storage and reads public DNS for validation.
// It performs no key generation/rotation, local DNS handler registration,
// DNS record write/delete, or provisioning fallback.

🪵 Logging

smartmta logs through its own Smartlog instance, exported as baseLogger. Console output is enabled by default so an embedded smartmta never logs into the void. Embedders can attach additional destinations (e.g. an ops log buffer):

import { baseLogger } from '@push.rocks/smartmta';

baseLogger.addLogDestination(myLogDestination); // any smartlog ILogDestination

♻️ Bridge Resilience

The Rust mailer-bin process is supervised by RustSecurityBridge:

  • Concurrent command handling — outbound SMTP sends run as detached tasks in the Rust management loop; an in-flight delivery never blocks health-check pings, recipient-policy callbacks, or inbound event forwarding.
  • Health checks — a ping every 30 s; two consecutive failures kill and restart the process.
  • Auto-restart — any unexpected exit triggers restart with exponential backoff (1 s → 30 s). After maxRestartAttempts fast attempts the bridge enters the failed state but keeps retrying at the slow cadence forever — it never silently stays dead.
  • SMTP server restore — after a restart the SMTP listener is restored; if the restore fails, the process is killed and the restart cycle continues rather than reporting a half-alive bridge.
  • State surfaceUnifiedEmailServer.getBridgeState() returns 'idle' | 'starting' | 'running' | 'restarting' | 'failed' | 'stopped'; the server emits bridgeFailed, bridgeRestarting, and bridgeRecovered events for embedders' alerting. isSmtpListenerActive() reports whether the Rust SMTP listener is accepting connections, and a failed listener reconfiguration emits smtpListenerRestartFailed.

Tune with RustSecurityBridge.configure({ maxRestartAttempts, healthCheckIntervalMs, healthCheckTimeoutMs, restartBackoffBaseMs, restartBackoffMaxMs }).

🦀 Rust Acceleration Layer

Performance-critical operations are implemented in Rust and communicate with the TypeScript runtime via @push.rocks/smartrust (JSON-over-stdin/stdout IPC). The Rust workspace lives at rust/ with four crates:

| Crate | Status | Purpose | |---|---|---| | mailer-core | Complete | Email types, validation, MIME building, bounce detection | | mailer-security | Complete | DKIM sign/verify, SPF, DMARC, IP reputation/DNSBL, content scanning | | mailer-smtp | Complete | Full SMTP protocol engine — TCP/TLS server + client, STARTTLS, AUTH, pipelining, connection pooling, in-process security pipeline | | mailer-bin | Complete | CLI + smartrust IPC bridge — wires everything together |

What Runs Where

| Operation | Runs In | Why | |---|---|---| | SMTP server (port listening, protocol, TLS) | 🦀 Rust | Performance, memory safety, zero-copy parsing | | SMTP client (outbound delivery, connection pooling) | 🦀 Rust | Connection management, TLS negotiation | | DKIM signing & verification | 🦀 Rust | Crypto-heavy, benefits from native speed | | SPF validation | 🦀 Rust | DNS lookups with async resolver | | DMARC policy checking | 🦀 Rust | Integrates with SPF/DKIM results | | IP reputation / DNSBL | 🦀 Rust | Parallel DNS queries | | Content scanning (text patterns) | 🦀 Rust | Regex engine performance | | Bounce detection (pattern matching) | 🦀 Rust | Regex engine performance | | Email validation & MIME building | 🦀 Rust | Parsing performance | | Email routing & orchestration | 🟦 TypeScript | Business logic, flexibility | | Delivery queue & retry | 🟦 TypeScript | State management, persistence | | Template rendering | 🟦 TypeScript | String interpolation | | Domain & DNS management | 🟦 TypeScript | API integrations |

📁 Project Structure

smartmta/
├── ts/                              # TypeScript source
│   ├── mail/
│   │   ├── core/                    # Email, EmailValidator, BounceManager, TemplateManager
│   │   ├── delivery/                # DeliveryQueue, DeliverySystem, RateLimiter
│   │   ├── routing/                 # UnifiedEmailServer, EmailRouter, DomainRegistry, DnsManager
│   │   └── security/                # DKIMCreator, SpfVerifier
│   └── security/                    # ContentScanner, IPReputationChecker, RustSecurityBridge
├── rust/                            # Rust workspace
│   └── crates/
│       ├── mailer-core/             # Email types, validation, MIME, bounce detection
│       ├── mailer-security/         # DKIM, SPF, DMARC, IP reputation, content scanning
│       ├── mailer-smtp/             # Full SMTP server + client (TCP/TLS, rate limiting, pooling)
│       └── mailer-bin/              # CLI + smartrust IPC bridge
├── test/                            # TypeScript test suite
├── dist_ts/                         # Compiled TypeScript output
└── dist_rust/                       # Compiled Rust binaries

🧪 Testing

The project has comprehensive test coverage with both unit and end-to-end tests:

# Build Rust binary first
pnpm build

# Run all tests
pnpm test

# Run specific test files
tstest test/test.e2e.server-lifecycle.node.ts --verbose --timeout 60
tstest test/test.e2e.inbound-smtp.node.ts --verbose --timeout 60
tstest test/test.e2e.routing-actions.node.ts --verbose --timeout 60
tstest test/test.e2e.outbound-delivery.node.ts --verbose --timeout 60

E2E tests exercise the full pipeline — starting UnifiedEmailServer, connecting via raw TCP sockets, sending SMTP transactions, verifying routing actions, and testing outbound delivery through a mock SMTP receiver.

API Reference

Exported Classes (top-level)

| Class | Description | |---|---| | UnifiedEmailServer | 🎯 Main entry point — orchestrates SMTP server, routing, security, and delivery | | Email | Email message class with validation, attachments, headers, and RFC 822 serialization | | EmailRouter | Pattern-based route matching and evaluation engine | | DomainRegistry | Multi-domain configuration manager | | DnsManager | Automatic DNS record management | | DKIMCreator | DKIM key generation, storage, rotation | | SpfVerifier | Lightweight SPF record parser |

Namespaced Exports

| Namespace | Classes | |---|---| | Core | Email, EmailValidator, TemplateManager, BounceManager | | Delivery | UnifiedDeliveryQueue, MultiModeDeliverySystem, DeliveryStatus, UnifiedRateLimiter |

License and Legal Information

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license file.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

Company Information

Task Venture Capital GmbH Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at [email protected].

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.