> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/puiusabin/bun-smtp/llms.txt
> Use this file to discover all available pages before exploring further.

# TLS & STARTTLS

> Configure TLS encryption for secure SMTP connections

## Overview

bun-smtp supports two TLS modes for securing SMTP connections:

* **Implicit TLS** — TLS encryption from the first byte (port 465)
* **STARTTLS** — Plain connection upgraded to TLS on demand (ports 25, 587)

## Implicit TLS

Implicit TLS (also called SMTPS) establishes an encrypted connection immediately. This is typically used on port 465.

<Steps>
  <Step title="Set secure: true">
    Enable implicit TLS mode:

    ```typescript theme={null}
    const server = new SMTPServer({
      secure: true,
    });
    ```
  </Step>

  <Step title="Provide certificates">
    Add your TLS certificate and private key:

    ```typescript theme={null}
    import { readFileSync } from "node:fs";

    const server = new SMTPServer({
      secure: true,
      key: readFileSync("server.key"),
      cert: readFileSync("server.crt"),
      onData(stream, session, callback) {
        stream.pipeTo(new WritableStream()).then(() => callback(null), callback);
      },
    });
    ```
  </Step>

  <Step title="Listen on port 465">
    Start the server on the standard SMTPS port:

    ```typescript theme={null}
    await server.listen(465);
    console.log("SMTPS server listening on port 465");
    ```
  </Step>
</Steps>

<Tip>
  With Bun, you can use `Bun.file()` instead of `readFileSync` for better performance:

  ```typescript theme={null}
  const server = new SMTPServer({
    secure: true,
    key: await Bun.file("server.key").text(),
    cert: await Bun.file("server.crt").text(),
  });
  ```
</Tip>

## STARTTLS

STARTTLS allows clients to upgrade a plain connection to TLS. This is advertised in the EHLO response and is the standard for ports 25 and 587.

```typescript theme={null}
import { readFileSync } from "node:fs";

const server = new SMTPServer({
  key: readFileSync("server.key"),
  cert: readFileSync("server.crt"),
  onData(stream, session, callback) {
    stream.pipeTo(new WritableStream()).then(() => callback(null), callback);
  },
});

await server.listen(587);
```

<Info>
  When `key` and `cert` are provided but `secure` is not set (or is `false`), STARTTLS is automatically advertised in the EHLO capabilities.
</Info>

### Hiding STARTTLS

To support STARTTLS without advertising it in EHLO (clients can still use it if they know about it):

```typescript theme={null}
const server = new SMTPServer({
  key: readFileSync("server.key"),
  cert: readFileSync("server.crt"),
  hideSTARTTLS: true,
});
```

## Requiring STARTTLS

Force clients to complete STARTTLS before sending AUTH or MAIL commands:

```typescript theme={null}
const server = new SMTPServer({
  needsUpgrade: true,
  key: readFileSync("server.key"),
  cert: readFileSync("server.crt"),
});
```

<Warning>
  Clients that attempt AUTH or MAIL before upgrading receive:

  ```
  530 5.7.0 Must issue a STARTTLS command first
  ```
</Warning>

## Development Mode (No Certificate)

When no `key` or `cert` is provided, bun-smtp uses a built-in self-signed certificate. This lets you test TLS functionality without certificate setup:

```typescript theme={null}
const server = new SMTPServer({ 
  authOptional: true,
});

await server.listen(2525);
// STARTTLS works immediately — no cert setup required
```

<Warning>
  **Never use the built-in certificate in production.** It's self-signed and provides no security guarantees. Always use proper certificates from a trusted CA or Let's Encrypt.
</Warning>

<Accordion title="View the built-in certificate details">
  The built-in certificate is a self-signed localhost certificate from the original smtp-server package:

  ```typescript src/smtp-server.ts theme={null}
  const DEFAULT_TLS_CERT =
    "-----BEGIN CERTIFICATE-----\n" +
    "MIICpDCCAYwCCQCuVLVKVTXnAjANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDEwls\n" +
    "b2NhbGhvc3QwHhcNMTUwMjEyMTEzMjU4WhcNMjUwMjA5MTEzMjU4WjAUMRIwEAYD\n" +
    // ... truncated for brevity
  ```

  Subject: `CN=localhost`
  Valid: 2015-02-12 to 2025-02-09
</Accordion>

## SNI (Server Name Indication)

Serve different certificates for different hostnames using SNI:

```typescript theme={null}
const server = new SMTPServer({
  sniOptions: {
    "mail.example.com": {
      key: readFileSync("example-com.key"),
      cert: readFileSync("example-com.crt"),
    },
    "mail.other.org": {
      key: readFileSync("other-org.key"),
      cert: readFileSync("other-org.crt"),
    },
  },
});
```

<Info>
  `sniOptions` accepts either a plain object or a `Map<string, TLSOptions>` for dynamic certificate management.
</Info>

### Dynamic SNI with Map

Use a `Map` for runtime certificate updates:

```typescript theme={null}
const sniMap = new Map<string, TLSOptions>();

sniMap.set("mail.example.com", {
  key: readFileSync("example-com.key"),
  cert: readFileSync("example-com.crt"),
});

const server = new SMTPServer({
  sniOptions: sniMap,
});

// Add a new domain at runtime
sniMap.set("mail.newdomain.com", {
  key: readFileSync("newdomain.key"),
  cert: readFileSync("newdomain.crt"),
});
```

## Validating the TLS Handshake

Use `onSecure` to inspect or reject connections after TLS is established:

```typescript theme={null}
const server = new SMTPServer({
  requestCert: true,
  onSecure(socket, session, callback) {
    // Access TLS details via session.tlsOptions
    console.log("Cipher:", session.tlsOptions?.name);
    console.log("Protocol:", session.tlsOptions?.version);
    
    // Reject connections based on TLS properties
    if (session.tlsOptions?.version === "TLSv1.0") {
      return callback(new Error("TLS 1.0 is not supported"));
    }
    
    callback(null);
  },
});
```

<Note>
  `onSecure` is called after both implicit TLS and STARTTLS upgrades. The `socket` parameter is a Bun `Socket`, not a Node.js `tls.TLSSocket`.
</Note>

## TLS Options Reference

| Option               | Type                                                    | Description                                   |
| -------------------- | ------------------------------------------------------- | --------------------------------------------- |
| `key`                | `string \| Buffer`                                      | Private key in PEM format                     |
| `cert`               | `string \| Buffer`                                      | Certificate in PEM format                     |
| `ca`                 | `string \| Buffer \| Array`                             | CA certificates for client verification       |
| `requestCert`        | `boolean`                                               | Request a client certificate during handshake |
| `rejectUnauthorized` | `boolean`                                               | Reject clients with invalid certificates      |
| `minVersion`         | `string`                                                | Minimum TLS version (e.g., `"TLSv1.2"`)       |
| `maxVersion`         | `string`                                                | Maximum TLS version (e.g., `"TLSv1.3"`)       |
| `sniOptions`         | `Record<string, TLSOptions> \| Map<string, TLSOptions>` | Per-hostname TLS configuration                |

## Client Certificate Authentication

Require and validate client certificates:

<Steps>
  <Step title="Enable client certificates">
    ```typescript theme={null}
    const server = new SMTPServer({
      key: readFileSync("server.key"),
      cert: readFileSync("server.crt"),
      ca: readFileSync("ca.crt"),
      requestCert: true,
      rejectUnauthorized: true,
    });
    ```
  </Step>

  <Step title="Validate in onSecure">
    ```typescript theme={null}
    onSecure(socket, session, callback) {
      // In Bun, client cert details aren't directly exposed on the socket
      // You can implement custom validation based on your requirements
      callback(null);
    }
    ```
  </Step>
</Steps>

## Updating Certificates at Runtime

Rotate certificates without restarting the server:

```typescript theme={null}
// Initial setup
const server = new SMTPServer({
  key: readFileSync("server.key"),
  cert: readFileSync("server.crt"),
});

await server.listen(587);

// Later, update certificates (e.g., after Let's Encrypt renewal)
server.updateSecureContext({
  key: readFileSync("new-server.key"),
  cert: readFileSync("new-server.crt"),
});
```

<Info>
  New connections will use the updated certificates immediately. Existing connections continue using the old certificates until they close.
</Info>

## Port Recommendations

<CardGroup cols={3}>
  <Card title="Port 25" icon="envelope">
    **MTA-to-MTA**

    Traditional SMTP port for server-to-server communication. Usually supports STARTTLS but doesn't require it.
  </Card>

  <Card title="Port 587" icon="paper-plane">
    **Message Submission**

    Standard port for client-to-server communication. Should require STARTTLS and authentication.
  </Card>

  <Card title="Port 465" icon="lock">
    **SMTPS**

    Implicit TLS from connection start. Use `secure: true` for this port.
  </Card>
</CardGroup>

## Complete Examples

<Tabs>
  <Tab title="Production (Port 587)">
    ```typescript theme={null}
    import { SMTPServer } from "bun-smtp";
    import { readFileSync } from "node:fs";

    const server = new SMTPServer({
      // Require STARTTLS before auth/mail
      needsUpgrade: true,
      key: readFileSync("/etc/ssl/private/mail.key"),
      cert: readFileSync("/etc/ssl/certs/mail.crt"),
      
      // Require authentication
      authOptional: false,
      allowInsecureAuth: false,
      
      // Enforce modern TLS
      minVersion: "TLSv1.2",
      
      onAuth(auth, session, callback) {
        // Verify credentials
        if (verifyUser(auth.username, auth.password)) {
          callback(null, { user: auth.username });
        } else {
          callback(new Error("Invalid credentials"));
        }
      },
      
      onData(stream, session, callback) {
        // Process message
        processMessage(stream, session)
          .then(() => callback(null))
          .catch(callback);
      },
    });

    await server.listen(587);
    console.log("SMTP server listening on port 587");
    ```
  </Tab>

  <Tab title="SMTPS (Port 465)">
    ```typescript theme={null}
    import { SMTPServer } from "bun-smtp";
    import { readFileSync } from "node:fs";

    const server = new SMTPServer({
      // Implicit TLS
      secure: true,
      key: readFileSync("/etc/ssl/private/mail.key"),
      cert: readFileSync("/etc/ssl/certs/mail.crt"),
      
      authOptional: false,
      minVersion: "TLSv1.2",
      
      onAuth(auth, session, callback) {
        if (verifyUser(auth.username, auth.password)) {
          callback(null, { user: auth.username });
        } else {
          callback(new Error("Invalid credentials"));
        }
      },
      
      onData(stream, session, callback) {
        processMessage(stream, session)
          .then(() => callback(null))
          .catch(callback);
      },
    });

    await server.listen(465);
    console.log("SMTPS server listening on port 465");
    ```
  </Tab>

  <Tab title="Development">
    ```typescript theme={null}
    import { SMTPServer } from "bun-smtp";

    const server = new SMTPServer({
      // No TLS config = uses built-in self-signed cert
      authOptional: true,
      
      onData(stream, session, callback) {
        // Just log the message
        console.log("Received message from", session.envelope.mailFrom?.address);
        stream.pipeTo(new WritableStream()).then(() => callback(null), callback);
      },
    });

    await server.listen(2525);
    console.log("Development SMTP server listening on port 2525");
    ```
  </Tab>
</Tabs>
