Skip to main content
Glama
igoyetche

Paperboy

by igoyetche
README.md
# Paperboy - Send to Kindle in one step

<div align="center">
  <img src="docs/assets/icons/main-icon.png" alt="Paperboy icon" width="400" />
</div>

<div align="center"> 

[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=paperboy&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=paperboy)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=paperboy&metric=bugs)](https://sonarcloud.io/summary/new_code?id=paperboy)
[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=paperboy&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=paperboy)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=paperboy&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=paperboy)
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=paperboy&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=paperboy)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=paperboy&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=paperboy)

</div>

Send Markdown (or pre-built EPUB) content to your Kindle — from the terminal, from Claude Code, or from any MCP client.

## How It Works

1. You provide Markdown content (a file, stdin, or via Claude) or an existing `.epub` file
2. Paperboy converts Markdown to EPUB (downloading and embedding remote images along the way); `.epub` files pass through untouched
3. The EPUB is emailed to your Kindle address
4. The document appears in your Kindle library

## Three Ways to Use It

### CLI

```bash
# From a Markdown file
paperboy --title "My Article" --file article.md

# From an existing EPUB (sent as-is, no conversion)
paperboy --file book.epub

# From stdin
cat article.md | paperboy --title "My Article"

# With options
paperboy --title "Notes" --file notes.md --author "Alice" --device "Alice's Kindle"
```

### Folder Watcher

Drop `.md` or `.epub` files into a watched folder — Paperboy converts (Markdown) or forwards (EPUB) and sends them automatically.

```bash
paperboy watch
```

### MCP Server

Connect Claude Desktop or any MCP client to the server. Claude can then send content to your Kindle in a single tool call during conversation.

## Prerequisites

- **Node.js 22** or later
- An **SMTP account** — any provider works; Gmail is the easiest option, see [Gmail setup](#gmail-setup) below
- Your **Kindle email address** (found in Amazon account settings under "Send to Kindle")
- The SMTP sender address must be in your [Amazon approved senders list](https://www.amazon.com/sendtokindle)

## Setup

```bash
git clone <repo-url>
cd send-to-kindle
npm install
npm run build
```

Copy `.env.example` to `.env` and fill in your values:

```bash
cp .env.example .env
```

> The `.env` file loads automatically when running locally. In Docker, set environment variables directly — they take precedence over `.env`.

### Required Environment Variables

| Variable | Description | Example |
|---|---|---|
| `KINDLE_DEVICES` | Named device(s) in `name:email` format | `personal:your-kindle@kindle.com` |
| `SENDER_EMAIL` | Email that sends the EPUB | `you@gmail.com` |
| `SMTP_HOST` | SMTP server hostname | `smtp.gmail.com` |
| `SMTP_PORT` | SMTP server port | `587` |
| `SMTP_USER` | SMTP login username | `you@gmail.com` |
| `SMTP_PASS` | SMTP app password | `abcd-efgh-ijkl-mnop` |

Multiple devices: `KINDLE_DEVICES=personal:you@kindle.com,partner:them@kindle.com`

### Optional Environment Variables

| Variable | Default | Description |
|---|---|---|
| `DEFAULT_AUTHOR` | `Claude` | Author name when none is specified |
| `WATCH_FOLDER` | — | Path to folder for the `paperboy watch` command (auto-sends files) |
| `MCP_HTTP_PORT` | — | Enables HTTP/SSE transport on this port |
| `MCP_AUTH_TOKEN` | — | Required when `MCP_HTTP_PORT` is set |
| `LOG_LEVEL` | `info` | Pino log level (`debug`, `info`, `warn`, `error`) |

## CLI Usage

### Flags

| Flag | Required | Description |
|------|----------|-------------|
| `--title <title>` | No | Title of the document sent to Kindle. If omitted, resolved from EPUB metadata (for `.epub`), Markdown frontmatter, or the filename stem |
| `--file <path>` | No | Path to a `.md` or `.epub` file; reads Markdown from stdin if omitted |
| `--author <name>` | No | Author name embedded in the EPUB (default: configured value) |
| `--device <name>` | No | Target Kindle device name (default: first configured device) |
| `--help` | No | Show usage text and exit |
| `--version` | No | Show version number and exit |

### Exit Codes

| Code | Meaning |
|------|---------|
| 0 | Document sent successfully |
| 1 | Validation error (unresolvable title, empty content, malformed frontmatter, size limit) |
| 2 | EPUB conversion error |
| 3 | Email delivery error (SMTP auth, connection, rejection) |
| 4 | Configuration error (missing or invalid environment variables) |

### Configuration Resolution

The CLI loads configuration in this order (first match wins):

1. Shell environment variables
2. `.env` file in the current working directory
3. `~/.paperboy/.env` fallback for global user configuration

`--help` and `--version` work without any configuration.

## MCP Server Usage

### Local (stdio transport)

```bash
npm run dev
```

Add to your Claude Desktop config (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "paperboy": {
      "command": "node",
      "args": ["dist/index.js"],
      "cwd": "/path/to/send-to-kindle",
      "env": {
        "KINDLE_DEVICES": "personal:your-kindle@kindle.com",
        "SENDER_EMAIL": "you@gmail.com",
        "SMTP_HOST": "smtp.gmail.com",
        "SMTP_PORT": "587",
        "SMTP_USER": "you@gmail.com",
        "SMTP_PASS": "your-app-password"
      }
    }
  }
}
```

### Remote (HTTP/SSE transport)

Set `MCP_HTTP_PORT` and `MCP_AUTH_TOKEN`, then start the server:

```bash
MCP_HTTP_PORT=3000 MCP_AUTH_TOKEN=your-secret npm run dev
```

The server accepts MCP requests at `POST /mcp` with Bearer token authentication.

### MCP Tool: `send_to_kindle`

| Parameter | Type | Required | Description |
|---|---|---|---|
| `title` | string | yes | Document title (appears in Kindle library) |
| `content` | string | yes | Document body in Markdown format |
| `author` | string | no | Author name (defaults to `DEFAULT_AUTHOR`) |
| `device` | string | no | Target Kindle device name |

### Docker

```bash
# Build
docker build -t paperboy .

# Run with stdio
docker run -i --env-file .env paperboy

# Run with HTTP/SSE
docker run -p 3000:3000 --env-file .env paperboy
```

Or with Docker Compose:

```bash
docker compose up
```

## Folder Watcher

The folder watcher monitors a directory for Markdown and EPUB files and automatically sends each one to your Kindle. Drop a `.md` or `.epub` file in — `.md` files are converted to EPUB and `.epub` files are sent as-is; in both cases the file moves to `sent/` when done (or `error/` if something goes wrong).

### Setup

Add `WATCH_FOLDER` to your `.env`:

```
WATCH_FOLDER=/path/to/your/kindle-inbox
```

The folder must exist before starting the watcher. The `sent/` and `error/` subdirectories are created automatically.

### Run

```bash
paperboy watch
```

Files already in the folder when the watcher starts are processed immediately. New files are picked up as they arrive.

### Run as a background service

Service templates are provided in `scripts/service-templates/`.

**Linux (systemd):**

```bash
# Edit the file and replace /path/to/npx with the output of: which npx
cp scripts/service-templates/paperboy-watcher.service ~/.config/systemd/user/
systemctl --user enable --now paperboy-watcher
systemctl --user status paperboy-watcher
journalctl --user -u paperboy-watcher   # view logs
```

**macOS (launchd):**

```bash
# Edit the file and replace /path/to/npx with the output of: which npx
cp scripts/service-templates/com.paperboy.watcher.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.paperboy.watcher.plist
# Logs: ~/Library/Logs/paperboy-watcher.log
```

**Windows (Task Scheduler):**

```powershell
# Edit the file and replace C:\path\to\npx.cmd with the output of: where npx
schtasks /create /tn "PaperboyWatcher" /xml "scripts\service-templates\windows-task.xml"

# Or as a one-liner (no XML edit needed):
schtasks /create /tn "PaperboyWatcher" /tr "\"C:\path\to\npx.cmd\" paperboy watch" /sc onlogon /rl limited
```

The task starts at login and restarts automatically on failure.

### How it works

- Watches only the root of `WATCH_FOLDER` (not subdirectories)
- Waits 2 seconds after a file stops changing before processing (safe for slow copies)
- Processes files one at a time
- Retries transient SMTP failures up to 3 times with exponential backoff
- Permanent errors (auth failure, rejection) are not retried
- Shuts down gracefully on SIGINT/SIGTERM, draining any in-progress file

## Security

**Dependency Scanning:** npm audit is enforced at two points:

1. **Pre-commit hook** — blocks commits if high/critical vulnerabilities are found
2. **CI/CD workflow** — blocks merges if high/critical vulnerabilities are found

Run locally: `npm run audit:ci` (exits non-zero if vulnerabilities present)

## Development

```bash
npm run dev            # Run MCP server with tsx (no build step)
npm run cli -- --help  # Run CLI with tsx
npm run build          # Compile TypeScript to dist/
npm test               # Run automated tests (335 tests across 31 files)
npm run test:watch     # Run tests in watch mode
npm run test:coverage  # Run tests with coverage report (lcov)
npm run sonar:local    # Run coverage + SonarCloud scan locally
npm run test:email     # Send a real test email to verify SMTP config
npm run audit:ci       # Check for high/critical npm vulnerabilities
npm run lint           # Lint sources with ESLint
```

### Gmail Setup

Any SMTP provider works. Gmail is recommended for personal use — it's free, requires no domain, and has no sending restrictions for individual use.

Gmail requires an **App Password** — your regular password will not work for SMTP.

1. Enable 2-Step Verification: Google Account > Security > 2-Step Verification
2. Create an App Password: Google Account > Security > App Passwords
3. Use the generated 16-character password as `SMTP_PASS` in `.env`

Then add your sender address to Amazon's approved list: Amazon Account > Manage Your Content and Devices > Preferences > Personal Document Settings > Approved Personal Document E-mail List.

## Architecture

Three-layer design with strict dependency inversion:

```
Application (MCP + CLI adapters) --> Domain (service, values, ports) <-- Infrastructure (EPUB, SMTP)
```

- **Domain**: Value objects (`Title`, `Author`, `MarkdownContent`, `EpubDocument`, `ImageStats`), service orchestration (Markdown convert-then-deliver and EPUB passthrough), port interfaces, typed errors with `Result<T, E>`
- **Infrastructure**: Markdown-to-EPUB conversion (`marked` + `sanitize-html` + `epub-gen-memory`), image processor (downloads remote images with SSRF-safe redirect handling, converts AVIF/WebP/HEIC to JPEG via `sharp`), cover generator (SVG → JPEG via `sharp`), YAML frontmatter parser (`gray-matter`), EPUB reader (title from `<dc:title>` via `jszip`), SMTP delivery (`nodemailer`) with retry/backoff, Pino logging, CLI content reader
- **Application**: MCP tool handler, CLI adapter (arg parsing, exit codes, orchestration), folder watcher (`chokidar`), two composition roots (`index.ts` for MCP, `cli-entry.ts` for CLI)

## License

MIT