Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 58 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,75 @@ on:

jobs:
test:
name: Check and Test
name: Check and Test (deno ${{ matrix.deno-version }})
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
# Pinned baseline plus the moving latest release, so a type or lint
# change in a newer Deno fails here instead of in the Publish job.
deno-version: [v2.5.4, v2.x]

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.5.4
deno-version: ${{ matrix.deno-version }}

- name: Run source checks
run: deno task check

- name: Run test suite
run: deno task test

smoke-node:
name: Smoke (node ${{ matrix.node-version }})
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
# 24 is the current LTS; 22 is the floor the package claims to support.
node-version: ['22', '24']

steps:
- name: Checkout code
uses: actions/checkout@v7

- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}

- name: Send a message on Node
run: deno task smoke:node

smoke-bun:
name: Smoke (bun)
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v7

- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Send a message on Bun
run: deno task smoke:bun
6 changes: 3 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ jobs:
id-token: write

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- uses: denoland/setup-deno@v2
with:
deno-version: v2.5.4

- name: Run lint and check
run: deno lint src/ && deno check src/
- name: Run source checks
run: deno task check

- name: Publish package
run: deno publish
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,40 @@ All notable changes to this project are documented in this file.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [0.4.0] - 2026-10-12

### Added

- Added Node and Bun support: the package now runs on Deno, Node `>=22` and Bun `>=1.1`. Node and Bun install it from JSR with `npx jsr add @neabyte/deno-mailer`, which serves the same source as ESM with type declarations
- Added `SmtpTransport` runtime abstraction with a native Deno implementation and a `node:net`/`node:tls` implementation selected at connect time
- Added `deno task bundle`, `deno task smoke:node` and `deno task smoke:bun`, which send a message through a bundled build against a loopback SMTP server on each runtime
- Added `connectionTimeoutMs` SMTP config option (default `30000`) bounding TCP connect and each TLS handshake
- Added `socketTimeoutMs` SMTP config option (default `60000`) bounding each server reply wait and socket write
- Added config validation for timeout options (integer between `1` and `2147483647`)
- Added loopback SMTP server test helper and transport timeout regression tests

### Changed

- **Breaking:** `SmtpConnectionState` now carries a single `transport: SmtpTransport | null` field instead of `conn: Deno.Conn | null` and `tlsConn: Deno.TlsConn | null`, removing `Deno.*` from the published type surface
- Updated the TLS upgrade to resolve only after the handshake completes, so one timeout covers the whole upgrade on every runtime
- Updated SMTP client to close its socket on timeout so pooled clients reconnect instead of reusing a dead connection
- Updated RCPT handling to fail the send on timeout instead of counting the recipient as rejected
- Updated implicit TLS (`secure: true`) to connect over TCP then start TLS, so connect and handshake are bounded separately
- Updated `SmtpAuth` to share `SmtpCommand` wire I/O instead of a duplicate reader and writer
- Updated CI to run the check and test tasks against both the pinned Deno release and the latest `v2.x`
- Updated `deno task check` to verify formatting instead of rewriting it; use `deno task fmt` to format

### Removed

- **Breaking:** Removed the exported `SmtpConnection` class. It was unreachable from `SmtpClient` and reimplemented the transport without read, write or connect timeouts, without partial-write handling, and without multi-line reply parsing

### Fixed

- Fixed `deno check` failing on Deno `>=2.6`, where the pooled idle timer handle is an object rather than a number
- Fixed SMTP writes ignoring partial socket writes, which truncated messages larger than one write (64 KiB over TLS) and left `send()` waiting indefinitely
- Fixed the Node and Bun TLS upgrade rejecting data buffered before the handshake, which would otherwise allow STARTTLS command injection
- Fixed the secured socket leaking when a TLS handshake exceeded `connectionTimeoutMs`; the abandoned handshake is now aborted and its socket closed
- Fixed TLS upgrades to an SMTP host given as an IP address, which `node:tls` rejects when the address is offered as an SNI name

## [0.3.0] - 2026-03-25

Expand Down
29 changes: 25 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

# Deno Mailer

Lightweight Deno SMTP mailer with flexible configuration and formatting.
Lightweight SMTP mailer for Deno, Node and Bun, with flexible configuration and formatting.

[![Deno](https://img.shields.io/badge/deno-%3E%3D2.x-000000?logo=deno&logoColor=white)](https://deno.com) [![JSR](https://jsr.io/badges/@neabyte/deno-mailer)](https://jsr.io/@neabyte/deno-mailer) [![CI](https://github.com/NeaByteLab/Deno-Mailer/actions/workflows/ci.yml/badge.svg)](https://github.com/NeaByteLab/Deno-Mailer/actions/workflows/ci.yml) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Deno](https://img.shields.io/badge/deno-%3E%3D2.x-000000?logo=deno&logoColor=white)](https://deno.com) [![Node](https://img.shields.io/badge/node-%3E%3D22-5FA04E?logo=nodedotjs&logoColor=white)](https://nodejs.org) [![Bun](https://img.shields.io/badge/bun-%3E%3D1.1-FBF0DF?logo=bun&logoColor=black)](https://bun.sh) [![JSR](https://jsr.io/badges/@neabyte/deno-mailer)](https://jsr.io/@neabyte/deno-mailer) [![CI](https://github.com/NeaByteLab/Deno-Mailer/actions/workflows/ci.yml/badge.svg)](https://github.com/NeaByteLab/Deno-Mailer/actions/workflows/ci.yml) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

</div>

Expand All @@ -20,15 +20,30 @@ Lightweight Deno SMTP mailer with flexible configuration and formatting.
- **Transfer encodings**: base64, 7bit, quoted-printable for attachment content
- **Calendar invitations**: generates ICS calendar payload for meeting invites
- **Structured send result**: returns message id, envelope, accepted, rejected, and response
- **Zero external runtime deps**: built with Deno native capabilities
- **Runs on Deno, Node and Bun**: one source, native `Deno.connect` on Deno and `node:net`/`node:tls` elsewhere
- **Zero external runtime deps**: built on runtime primitives only

## Installation

```bash
# Install package from JSR.
# Deno, from JSR.
deno add jsr:@neabyte/deno-mailer
```

```bash
# Node.
npx jsr add @neabyte/deno-mailer
```

```bash
# Bun.
bunx jsr add @neabyte/deno-mailer
```

Requires Deno `>=2.x`, Node `>=22` or Bun `>=1.1`. On Node and Bun, `jsr add` registers the
`@jsr` registry in your project and installs the same source as ESM with type declarations,
so the import specifier stays `@neabyte/deno-mailer`.

## Quick Start

```ts
Expand Down Expand Up @@ -65,6 +80,12 @@ console.log(result.messageId)
deno task check
```

```bash
# Bundle the library and send a message through it on Node and on Bun.
deno task smoke:node
deno task smoke:bun
```

## Test

```bash
Expand Down
29 changes: 22 additions & 7 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,35 @@ This document explains the full **Deno Mailer** usage flow, from SMTP configurat
## Installation

```bash
# Install package from JSR.
# Deno.
deno add jsr:@neabyte/deno-mailer
```

Import the mailer from the same specifier your project uses for JSR (after `deno add`, the import map entry is usually `@neabyte/deno-mailer`):
```bash
# Node.
npx jsr add @neabyte/deno-mailer
```

```bash
# Bun.
bunx jsr add @neabyte/deno-mailer
```

Requires Deno `>=2.x`, Node `>=22` or Bun `>=1.1`. On Node and Bun, `jsr add` registers the
`@jsr` registry in your project and installs the library as ESM with type declarations, so the
import specifier is the same on every runtime.

```ts
import { mailer } from 'jsr:@neabyte/deno-mailer'
// or: import { mailer } from '@neabyte/deno-mailer'
import { mailer } from '@neabyte/deno-mailer'
// Deno also accepts the fully qualified form:
// import { mailer } from 'jsr:@neabyte/deno-mailer'
```

Types ship with the package. Use `import type { EmailMessage, SmtpConnectionConfig, SmtpSendResult } from 'jsr:@neabyte/deno-mailer'` when you only need types.
Types ship with the package. Use `import type { EmailMessage, SmtpConnectionConfig, SmtpSendResult } from '@neabyte/deno-mailer'` when you only need types.

> [!TIP]
> After `deno add`, prefer the import map key (often `@neabyte/deno-mailer`) so paths stay short and consistent.
> The library opens sockets with `Deno.connect` on Deno and with `node:net` on Node and Bun. On
> Deno this needs `--allow-net`; no permission flags apply on Node or Bun.

## Configuration

Expand Down Expand Up @@ -119,7 +133,8 @@ const transporter = mailer.transporter({
dkim: {
domainName: 'example.com',
keySelector: 'mail',
privateKey: Deno.env.get('DKIM_PRIVATE_KEY') || ''
// Deno: Deno.env.get('DKIM_PRIVATE_KEY'); Node and Bun: process.env.DKIM_PRIVATE_KEY
privateKey: readDkimPrivateKey()
}
})
```
Expand Down
13 changes: 9 additions & 4 deletions deno.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@neabyte/deno-mailer",
"description": "Lightweight Deno SMTP mailer with flexible configuration and formatting",
"version": "0.3.0",
"description": "Lightweight SMTP mailer for Deno, Node and Bun with flexible configuration and formatting",
"version": "0.4.0",
"type": "module",
"license": "MIT",
"exports": "./src/index.ts",
Expand Down Expand Up @@ -61,13 +61,18 @@
"exclude": ["tests/**/*.d.ts"]
},
"tasks": {
"check": "deno fmt src/ && deno lint src/ && deno check src/",
"test": "deno test --allow-net --allow-env tests/"
"fmt": "deno fmt src/ tests/ scripts/",
"check": "deno fmt --check src/ tests/ scripts/ && deno lint src/ && deno check src/ tests/",
"test": "deno test --allow-net --allow-env tests/",
"bundle": "deno bundle --platform=deno --packages=external --format=esm src/index.ts -o dist/mailer.mjs",
"smoke:node": "deno task bundle && node scripts/smoke.mjs",
"smoke:bun": "deno task bundle && bun scripts/smoke.mjs"
},
"imports": {
"@std/assert": "jsr:@std/assert@^1.0.19",
"@app/": "./src/",
"@smtp/": "./src/smtp/",
"@runtime/": "./src/runtime/",
"@utils/": "./src/utils/",
"@tests/": "./tests/"
},
Expand Down
Loading
Loading