Signal and Gmail document ingestion for Paperless-ngx.
Stop drowning in paper. Hundreds of emails with receipts, contracts, and attachments buried in your Gmail, or physical docs piling up on your desk? Scan or snap, send via Signal or email, and let Paperless-ngx do the rest. Optional AI (local Ollama) filters junk; pair with paperless-ai for tags and summaries. Find any document when you need it.
Privacy-first: Runs fully locally. No cloud APIs, no telemetry. Signal is a privacy-focused IM; AI uses local Ollama. Your documents stay on your machine.
Setup is a bit involved. Geeks will feel at home - determined non-geeks can get there too. Linux and macOS, Windows may work (WSL could help).
- Paperless-ngx (API upload)
- For Signal: signal-cli-rest-api must be running first
- For Gmail: Signal must be set up first (accounts are added via Signal commands)
- Signal: Webhook server for document attachments sent via Signal.
- Gmail: IMAP-based crawl for email attachments. One-shot (runs once, exits). Schedule with cron or systemd timer.
- Ollama: Optional AI eligibility filter for email attachments (images, plain text).
- Auto-PR: Push to
ai/**branches to auto-create PRs with titles from conventional commits via knirski/auto-pr.
Supported file types: PDF, Word (.doc, .docx), RTF, Office formats, images (JPEG, PNG, etc.), plain text, HTML, CSV.
Might add: other email providers (Outlook, Proton), other IMs (Matrix, Telegram), cloud storage, scanners, fax.
See docs/RELATED_PROJECTS.md for a list of projects that complement or extend this bot.
| Dependency | Required | Purpose |
|---|---|---|
| Paperless-ngx | Yes | Document management system; bot uploads via REST API |
| Ollama | No | Optional AI eligibility assessment; also used for AI-generated PR titles in the auto-PR workflow (runs locally) |
| paperless-ai | No | Optional AI post-processing (tags, titles, correspondents). This bot uses Ollama directly for pre-ingestion; paperless-ai augments after ingestion |
| signal-cli-rest-api | For Signal | Webhook server for Signal Messenger |
| Bun ≥ 1.3 | Yes | Runtime. See Bun support policy below. |
Install below, then follow Setup (Signal or Gmail) before running commands.
Bun or Nix:
Bun (from source):
git clone https://github.com/knirski/paperless-ingestion-bot.git
cd paperless-ingestion-bot
bun install && bun run buildNix:
nix build .#default # Build package
nix develop # Development shell
nix run .#default -- signal --config /path/to/config.jsonDocker:
Images are published to GHCR on each release. Release images are signed with Sigstore keyless signing; see docs/CI.md for verification steps. Use Compose — minimal (Signal + ingestion bot) or full-stack (Paperless + Signal + Ollama) — or run standalone:
docker run --rm \
-v /path/to/config:/etc/paperless-ingestion-bot:ro \
-v /path/to/data:/var/lib/paperless-ingestion-bot \
ghcr.io/knirski/paperless-ingestion-bot:latest signalMount your config directory at /etc/paperless-ingestion-bot (must contain config.json) and a data directory at /var/lib/paperless-ingestion-bot (for email-accounts.json, users.json). For Gmail, headless Linux requires a system credential store (libsecret/Secret Service); see Troubleshooting.
Env overrides: Override file values with individual env vars (e.g. -e PAPERLESS_INGESTION_SIGNAL_API_URL=http://signal:8080). Use --skip-reachability-check when the Signal API starts after the bot (e.g. Docker Compose).
Deployment: deploy/ — Compose (minimal or full-stack) and systemd (service + timer units).
paperless-ingestion-bot signal: Run Signal webhook server (validatespaperless_urlandsignal_api_urlat startup; use--skip-reachability-checkto bypass API reachability check)paperless-ingestion-bot email: Scan Gmail inboxes for attachments (one-shot; use with cron/systemd timer)paperless-ingestion-bot --help: Show helppaperless-ingestion-bot --version: Show versionpaperless-ingestion-bot email --json: Output{ "saved": N }to stdout for scriptingpaperless-ingestion-bot --completions <bash|zsh|fish>: Print shell completion script (append to~/.bashrc,~/.zshrc, or~/.config/fish/config.fish)
- Run signal-cli-rest-api (e.g. via Docker).
- Link your device: open
http://<signal-api-host>/v1/qrcodelinkin a browser. - Configure the webhook URL in signal-cli-rest-api:
RECEIVE_WEBHOOK_URL=http://<ingestion-bot-host>:<port>/webhook. - Create
users.jsonat--userspath or default (see Config schema below). Do not commit this file. - Start the ingestion bot:
paperless-ingestion-bot signal --config /path/to/config.json.
- Enable IMAP in Gmail settings.
- Create an App Password (requires 2-Step Verification).
- Add the account via Signal: send
gmail add user@example.com <app_password>to the linked Signal number. - Run the email pipeline (manually or via timer):
paperless-ingestion-bot email --config /path/to/config.json.
Generic IMAP is supported but adding accounts via Signal isn't implemented yet. Add entries manually to email-accounts.json (array of account objects). Each needs email, enabled, removed, exclude_labels, added_by, and details with type: "generic_imap", host, port, secure, mailbox. Gmail passwords go in the system keychain via gmail add; for generic IMAP you add credentials to the system keychain yourself.
JSON config (often Nix-generated via builtins.toJSON). Standalone: no parent-repo structure, just clone and run.
Resolution: --config flag or PAPERLESS_INGESTION_CONFIG env or default /etc/paperless-ingestion-bot/config.json. 12-factor: individual values and paths override via env (e.g. PAPERLESS_INGESTION_SIGNAL_API_URL).
Env overrides (12-factor): Individual env vars override file values when set: PAPERLESS_INGESTION_PAPERLESS_URL, PAPERLESS_INGESTION_PAPERLESS_TOKEN, PAPERLESS_INGESTION_SIGNAL_API_URL, PAPERLESS_INGESTION_WEBHOOK_HOST, PAPERLESS_INGESTION_WEBHOOK_PORT, PAPERLESS_INGESTION_LOG_LEVEL, etc. See schema for full list.
{
"paperless_url": "http://127.0.0.1:8000",
"paperless_token": "your-paperless-api-token",
"signal_api_url": "http://127.0.0.1:8080",
"log_level": "INFO",
"webhook_host": "127.0.0.1",
"webhook_port": 8089,
"ollama_url": "http://127.0.0.1:11434",
"ollama_vision_model": "moondream",
"ollama_text_model": "llama3.2",
"mark_processed_label": "paperless",
"page_size": 50
}paperless_url: Paperless-ngx base URL (e.g.http://127.0.0.1:8000)paperless_token: Paperless API token (Settings → Users → Create token)- Paperless API version contract: The bot targets a specific Paperless-ngx REST API version via the
Accept: application/json; version=Nheader. SeePAPERLESS_NGX_ACCEPT_VERSIONinsrc/live/paperless-client.ts. Your paperless-ngxALLOWED_VERSIONS(Settings → General) must include this version. signal_api_url: signal-cli-rest-api base URL (e.g.http://127.0.0.1:8080)- Config path:
--configorPAPERLESS_INGESTION_CONFIG(default:/etc/paperless-ingestion-bot/config.json). - Users path:
--usersorPAPERLESS_INGESTION_USERS_PATH(default:/var/lib/paperless-ingestion-bot/users.json). Create manually, don't commit. - Email accounts path:
--email-accountsorPAPERLESS_INGESTION_EMAIL_ACCOUNTS_PATH(default:/var/lib/paperless-ingestion-bot/email-accounts.json). Gmail/IMAP account metadata; passwords in system keychain. webhook_host,webhook_port: Signal webhook server bind addressollama_url,ollama_vision_model,ollama_text_model: Ollama endpoint and models for AI eligibility (optional)log_level:DEBUG|INFO|WARN|ERRORmark_processed_label: Gmail label for processed messages; empty string disables labelingpage_size: Email fetch batch size
Env overrides: Each key can be overridden by an env var: PAPERLESS_INGESTION_ + UPPER_SNAKE_CASE of the key (e.g. paperless_url → PAPERLESS_INGESTION_PAPERLESS_URL).
See config.example.json for a full example. A JSON Schema is emitted at build time: dist/config.schema.json.
users.json format (array of user objects):
[
{
"slug": "krzysiek",
"signal_number": "+48123456789",
"display_name": "Krzysiek"
}
]slug: Unique identifier; Paperless tag issignal-{slug}(e.g.signal-krzysiek)signal_number: User's Signal phone numberdisplay_name: Human-readable name
Security-first design. This project takes supply chain and operational security seriously:
- Credentials — Stored only in the OS keychain (@napi-rs/keyring). No file fallback; fails clearly when keyring unavailable. Gmail app passwords and Signal API access never touch disk in plaintext.
- Webhook — Token-bucket rate limiting (120/min); excess returns 429. Same-host deployment recommended: bind to
127.0.0.1so only local processes (signal-cli-rest-api) can reach it. See ADR 0002. - PII in errors — Paths, emails, phones, URLs are redacted in logs via Effect
Redacted; raw values never appear in structured logs. - Supply chain —
bun audit --audit-level=highin every check. CycloneDX SBOM generated in CI. Dependabot, CodeQL, OpenSSF Scorecard with least-privilege workflow permissions. Release images signed with Sigstore/cosign keyless signing. - File permissions — email-accounts metadata written with mode
0600. Run as a dedicated user.
Trust model: All family members in the config share the same Gmail account registry. Anyone can run gmail status to see all configured accounts and use pause/resume/remove on any of them. The design assumes a trusted group.
| Issue | Solution |
|---|---|
System keychain unavailable or File system error: getPassword at keyring |
Ensure libsecret/Secret Service is available (e.g. gnome-keyring, kwallet). On headless Linux, run a secret service or use a session with keychain support. |
Config file not found |
Pass --config /path/to/config.json or set PAPERLESS_INGESTION_CONFIG. |
paperless_url not reachable |
Ensure Paperless-ngx is running. Use --skip-reachability-check to bypass (e.g. Paperless starts after bot). |
signal_api_url not reachable |
Ensure Signal REST API is running. Use --skip-reachability-check to bypass (e.g. API starts after bot). |
No users configured |
Create users.json at --users path or PAPERLESS_INGESTION_USERS_PATH: [{"slug":"krzysiek","signal_number":"+48...","display_name":"Krzysiek"},...]. |
No account found for gmail commands |
Run gmail status to list accounts. Add with gmail add email@example.com <app_password>. |
| IMAP connection fails | Enable IMAP in Gmail; use App Password, not main password. |
| Ollama assessment timeout | Increase model load or reduce prompt; timeout is 60s. |
bun run checkRuns tests, lint, and typecheck.
CI: GitHub Actions runs bun run check on push and PR. Commits must follow Conventional Commits (enforced by commitlint). A CycloneDX SBOM is generated and uploaded as a workflow artifact.
TypeScript implementation using Effect and functional programming conventions. Bleeding edge: Effect v4 beta, TypeScript Native (tsgo) for build and typecheck (~10× faster than tsc).
We target Bun 1.3+ and Effect v4 beta. This is a deliberate choice for modern features. We intend to support the Bun version that Effect v4 officially supports. Check Effect compatibility and our packageManager field in package.json for the minimum supported version.
- deploy/: Deployment recipes — Compose (Docker) and systemd (service units)
- ARCHITECTURE.md: Project structure and design
- RELATED_PROJECTS.md: Potentially useful complementary projects
- CONTRIBUTING.md: How to contribute
- knirski/auto-pr: Auto-PR workflow (push to
ai/*branches) - docs/SCHEDULED_WORKFLOWS.md: Enable scheduled workflows (cron)
- CODE_OF_CONDUCT.md: Community standards
- SECURITY.md: Vulnerability reporting
- SUPPORT.md: Getting help
- test/integration/README.md: Integration test guide
- docs/plans/COMPLETED_SUMMARY.md: Summary of completed plans and designs
- docs/adr/: Architecture Decision Records
CII Best Practices: See docs/CII.md for progress. Complete the self-assessment to earn the badge.
This project was developed with assistance from AI coding tools.
Apache 2.0. See LICENSE.