I built this pipeline because I wanted a frictionless way to read my favorite newsletters offline on my Kindle Oasis. I've designed it to be fully self-hosted via GitHub Actions so you can own your data and read offline without friction.
Deliver your favorite newsletters (Substack, etc.) as clean, beautiful EPUBs directly to your Kindle. This pipeline uses a Google Sheet as a triage gateway, allowing you to approve or skip content before it reaches your device.
Built to be Forked: This repository is a personal automation template. You are encouraged to fork it, add your own credentials, and run your own private instance on GitHub Actions.
graph TD
%% Custom Styling to avoid yellow node backgrounds and keep theme neutral/light-grey
classDef greyNode fill:#f4f4f4,stroke:#ccc,stroke-width:1px,color:#333;
subgraph Ingestion ["Ingestion"]
Gmail["Gmail Inbox<br/>(Label: Substack-Kindle)"]
end
subgraph GHA ["GitHub Actions"]
Pipeline["Python Workflow<br/>(Runs every 30m)"]
Config["Configuration (Credentials)<br/>- GMAIL_USER / GMAIL_APP_PASSWORD<br/>- WEB_APP_URL / WEB_APP_SECRET"]
end
subgraph SheetMgmt ["Google Sheet for Management"]
AppScript["Google Apps Script<br/>(API Bridge)"]
Sheet["Google Sheet<br/>(Triage UI)"]
ScriptProps["Script Properties<br/>(Holds WEB_APP_SECRET)"]
end
subgraph Delivery ["Delivery"]
Kindle["Amazon Kindle<br/>(Allowlisted Intake)"]
end
%% Apply node styling
class Gmail,Pipeline,Config,AppScript,Sheet,ScriptProps,Kindle greyNode;
style Ingestion fill:#f9f9f9,stroke:#e4e4e7,stroke-width:1px
style GHA fill:#f9f9f9,stroke:#e4e4e7,stroke-width:1px
style SheetMgmt fill:#f9f9f9,stroke:#e4e4e7,stroke-width:1px
style Delivery fill:#f9f9f9,stroke:#e4e4e7,stroke-width:1px
%% Connections
Gmail -- "IMAP Sync" --> Pipeline
Pipeline -- "Reads Config" --> Config
Pipeline -- "Sends Newsletter Details" --> AppScript
AppScript -- "Validates Secret against" --> ScriptProps
AppScript -- "Writes Row" --> Sheet
Sheet -- "User marks APPROVED or SKIP" --> AppScript
Pipeline -- "Checks for Updates" --> AppScript
AppScript -- "Fetches Approved Newsletters" --> Pipeline
Pipeline -- "Emails EPUB File" --> Kindle
- Ingestion: A GitHub Action checks your Gmail for emails tagged with the
Substack-Kindlelabel (scanning the last 14 days). - Triage: Metadata (Subject, Author, Date) is sent to your Google Sheet with a
PENDINGstatus. - Approval: You manually mark rows as
APPROVEDorSKIPin the sheet. - Delivery: The next pipeline run fetches approved items, converts them to EPUB, emails them to your Kindle, and updates their status.
Every newsletter row in the Google Sheet progresses through specific statuses:
PENDING: A newly ingested newsletter waiting for your review.APPROVED: You've marked this row to be sent to your Kindle.SKIP: You've chosen to ignore/skip this newsletter.DELIVERED: The pipeline successfully sent the newsletter EPUB to your Kindle.PURGED: The newsletter was ignored and cleaned up (triggered bySKIP).ERROR: Something went wrong during fetching, conversion, or delivery (such as "ERROR: No Content").
Ready to get started? Follow our detailed step-by-step guide:
View the Detailed Setup Guide (docs/SETUP.md)
Substack-RFK is designed with a "security-first" mindset for personal automation:
- Scoped Access: By using App Passwords and a specific Gmail Label (
Substack-Kindle), the pipeline only interacts with newsletters you've explicitly tagged. It never reads your personal correspondence. - Burner Account Support: For maximum isolation, you can use a dedicated "burner" Gmail account and simply forward your newsletters to it.
- Encrypted Logs: Sensitive metadata (like newsletter subjects) is XOR-obfuscated in GitHub Action logs, ensuring your reading habits remain private even in public repositories.
- Extensibility: We welcome PRs to add support for other email providers (Outlook, Fastmail, etc.)! Check
src/mail_client.pyto see how to contribute.
- The pipeline runs automatically every 30 minutes and scans for any emails from the last 14 days in your
Substack-Kindlelabel. - You can trigger it manually from the Actions tab in GitHub.
- Check your Google Sheet daily to approve new newsletters!
If you pull new code from this repository and encounter a Version Mismatch error:
- Go to your Google Sheet ->
Extensions -> Apps Script. - Copy the latest code from
templates/Code.gsin this repo and paste it into the editor. - Click Save.
- CRITICAL: You must re-deploy for the changes to take effect. Click Deploy -> New Deployment.
- Select the same settings as before (Me / Anyone) and click Deploy.
- Update your
WEB_APP_URLin GitHub if the URL changed (it usually does for a "New Deployment").
Run the unit tests to ensure the pipeline is working correctly:
pip install -r requirements.txt
python3 -m pytestFound a bug or have a feature request? Please open a GitHub Issue.
MIT

