Completely streamline setup experience for new users, add more documentation

This commit is contained in:
Young Lee
2025-02-27 20:15:05 -08:00
parent 56a8263f33
commit 38f56eb7e2
6 changed files with 296 additions and 84 deletions
+4 -1
View File
@@ -25,4 +25,7 @@ yarn-error.log*
# System # System
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Cloudflare
wrangler.toml
+86 -45
View File
@@ -1,45 +1,88 @@
# Email-to-RSS # Email-to-RSS
A modern service that turns email newsletters into RSS feeds, built with Cloudflare Workers. This service provides unique email addresses per feed, a front-end admin panel, and long-term storage of newsletters. A modern service that turns email newsletters into RSS feeds, built with Cloudflare Workers and ForwardEmail.net. This service provides unique email addresses per feed, a front-end admin panel, and long-term storage of newsletters.
## Why Email to RSS?
I love consolidating my newsletters into a centralized reading app like [Reeder](https://reederapp.com). They make for a better reading experience and prevents my email inbox from getting clogged with newsletters. However, Reeder requires RSS support and many email newsletters don't support native RSS feeds.
There are some free services online that do the same thing (e.g. [kill-the-newsletter.com](kill-the-newsletter.com)), but there are several downsides to this approach:
- **No long-term retention**: old RSS posts are deleted to save space.
- **Risk of blocklisting**: being forced to use the same domain (@kill-the-newsletter.com) as everyone else increases the likelihood that an email newsletter can blocklist you from signing up.
- **Self-hosting is non-trivial**: Kill The Newsletter is also [open source](https://github.com/leafac/kill-the-newsletter), but the self-hosting steps seem neither straightforward nor does it focus on exclusively leveraging free services.
On the other hand, while Email-to-RSS isn't necessarily a one-click-deploy solution, it works just as well, is customized to your domain, and is completely free (except for the custom domain itself)!
## Features ## Features
- **Minimal Dependencies**: Built using modern, web-friendly libraries without Node.js-specific dependencies - **Autogenerate Custom Emails**: Creates custom email addresses in the format `noun1.noun2.XY@yourdomain.com` for each feed
- **Lightweight**: Entire worker bundle is only ~360KB (gzipped: ~65KB) - **ForwardEmail.net Integration**: Processes incoming emails via webhook with robust IP verification
- **Email Processing**: Handles emails from ForwardEmail.net webhook - **Minimalist Email Parser**: Custom-built lightweight parser that works efficiently in edge environments
- **RSS Generation**: Serves standards-compliant RSS feeds - **RSS Feed Generation**: Serves standards-compliant RSS feeds using the modern Feed library
- **Admin Interface**: Simple management UI for feeds and emails - **Admin Dashboard**: Complete web UI for managing feeds and viewing emails
- **Storage**: Uses Cloudflare KV for efficient, low-cost storage - **Secure Authentication**: Password-protected admin interface
- **Deletion Support**: Email content can be removed from feeds, with cache updates - **Cloudflare KV Storage**: Efficient, low-cost storage solution for feed data
- **Minimal Dependencies**: Built using modern, web-friendly libraries
- **Lightweight**: Entire worker bundle is optimized for edge deployment
- **Deletion Support**: Email content can be removed from feeds in the admin UI, with automatic cache updates
## Setup
1. Buy your custom domain. You can buy it directly from Cloudflare for convenience. If purchased elsewhere, add the domain to your Cloudflare account to manage the DNS there.
2. Clone this repository.
3. Open your command line in the cloned repo folder and run: `bash setup.sh` this will install dependencies and set up the Cloudflare Worker/KV with your admin password.
4. Set up your ForwardEmail.net account and configure it to forward to Cloudflare (replace `yourdomain.com` with your custom domain):
1. Sign up for a free account using any email (doesn't necessarily have to be the one used for the newsletter).
2. Add your custom domain.
3. To verify your domain, add the following DNS records to your Cloudflare DNS configuration:
| Type | Name | Content | TTL | Proxy Status | Notes |
| ---- | ---- | -------------------------------------------------- | ---- | ------------ | ---------------------------------------- |
| MX | @ | mx1.forwardemail.net | Auto | DNS only | Set Priority to 10. |
| MX | @ | mx2.forwardemail.net | Auto | DNS only | Set Priority to 10. |
| TXT | @ | "forward-email=https://yourdomain.com/api/inbound" | Auto | DNS only | This forwards your emails to the webhook |
| TXT | @ | "v=spf1 include:spf.forwardemail.net -all" | Auto | DNS only | Email security |
5. Deploy with `npm run deploy`.
6. Go to yourdomain.com to open up the admin panel and log in!
Tip: If you're unsure about any of these steps, ask ChatGPT or Cursor to guide you through them.
## Architecture ## Architecture
### Email Flow ### Email Flow
1. A newsletter arrives at `apple.mountain.42@yourdomain.com` (feed ID format: noun1.noun2.XX) 1. A newsletter email arrives at `apple.mountain.42@yourdomain.com` (feed ID format: noun1.noun2.XY).
2. ForwardEmail.net forwards it to your Cloudflare Worker 2. ForwardEmail.net forwards it to your Cloudflare Worker endpoint via webhook.
3. The Worker parses the email, extracts content, and stores it in KV 3. The Worker validates the request is from ForwardEmail.net based on IP address.
4. The RSS feed is updated with the new content 4. The email is parsed, content extracted, and stored in Cloudflare KV.
5. Feed metadata is updated to include the new email.
6. The RSS feed endpoint dynamically generates the feed from stored emails.
### Key Components ### Key Components
- **Email Parser**: Lightweight custom parser that works in edge environments - **Email Parser**: Extracts content from ForwardEmail.net webhook payload
- **Feed Generator**: Modern RSS feed generator with minimal dependencies - **Feed Generator**: Creates standard-compliant RSS feeds from stored emails
- **Admin UI**: Simple interface to manage feeds and view emails - **Admin UI**: Interface for creating, viewing, and managing feeds
- **ID Generator**: Creates memorable, collision-resistant feed IDs - **ID Generator**: Creates memorable, collision-resistant feed IDs using common nouns
- **Data Store**: Organized module for common nouns used in ID generation - **Security Layer**: Validates webhook requests against ForwardEmail.net IP addresses
- **Storage Manager**: Organized module for storing and retrieving data from KV
### Code Structure ### Code Structure
- **src/routes/**: API and UI route handlers - `src/routes/`: API and UI route handlers for inbound emails, RSS feeds, and admin panel
- **src/utils/**: Utility functions including email parsing and ID generation - `src/utils/`: Utility functions including email parsing, feed generation, and ID creation
- **src/data/**: Data files like the nouns list for feed IDs - `src/data/`: Data files including the nouns list used in ID generation
- **src/types/**: TypeScript type definitions - `src/types/`: TypeScript type definitions
- **src/index.ts**: Main application entry point - `src/scripts/`: Client-side JavaScript for the admin interface
- `src/styles/`: CSS styling for the admin interface
- `src/index.ts`: Main application entry point with middleware and routing configuration
## Development ## Development
This project uses a modern build process with Wrangler's built-in bundling (powered by esbuild): This project uses a modern build process with Cloudflare Wrangler's built-in bundling (powered by `esbuild`):
```bash ```bash
# Install dependencies # Install dependencies
@@ -55,27 +98,14 @@ npm run build
npm run deploy npm run deploy
``` ```
## Environment Variables
- `ADMIN_PASSWORD`: Password for the admin interface
- `DOMAIN`: Your custom domain for receiving emails
## Technology Stack ## Technology Stack
- **Cloudflare Workers**: Edge computing platform - **Cloudflare Workers**: Edge computing platform for running the service
- **Cloudflare KV**: Key-value storage - **Cloudflare KV**: Key-value storage for email and feed data
- **Hono**: Lightweight web framework - **Hono**: Lightweight web framework for routing and middleware
- **TypeScript**: Type-safe JavaScript - **TypeScript**: Type-safe JavaScript for reliable code
- **Feed**: Modern RSS feed generator - **Feed**: Modern RSS feed generator
- **Zod**: Schema validation - **Zod**: Schema validation for input data
## Setup
1. Clone this repository
2. Install dependencies with `npm install`
3. Copy `wrangler.toml.example` to `wrangler.toml` and set your values
4. Run `npm run dev` to start the development server
5. Deploy with `npm run deploy`
## Minimalist Approach ## Minimalist Approach
@@ -90,17 +120,28 @@ This project follows a minimalist approach:
## Feed ID System ## Feed ID System
The system generates memorable, user-friendly feed IDs in the format `noun1.noun2.XX` where: The system generates memorable, user-friendly feed IDs in the format `noun1.noun2.XY` where:
- `noun1` and `noun2` are randomly selected from a curated list of ~500 common nouns - `noun1` and `noun2` are randomly selected from a curated list of ~450 common, neutral nouns
- `XX` is a random two-digit number between 10 and 99 - `XY` is a random two-digit number between 10 and 99
This is inspired by iCloud's Hide My Email feature.
This format provides: This format provides:
- Easy to read and share email addresses - Easy to read and share email addresses
- Low collision probability (can handle thousands of feeds) - Low collision probability (can handle thousands of feeds)
- Simple to remember for users - Simple to remember for users
- ~22.5 million possible combinations - ~20 million possible combinations
### Noun Selection
The noun list has been carefully curated to:
- Include only common, everyday objects and concepts
- Exclude any potentially problematic terms
- Ensure appropriate combinations when nouns are randomly paired
- Maintain a professional appearance for all generated feed IDs
## License ## License
+1 -1
View File
@@ -7,7 +7,7 @@
"build": "wrangler deploy --dry-run --outdir=dist", "build": "wrangler deploy --dry-run --outdir=dist",
"format": "prettier --write '**/*.{js,ts,css,json,md}'", "format": "prettier --write '**/*.{js,ts,css,json,md}'",
"dev": "wrangler dev", "dev": "wrangler dev",
"deploy": "wrangler deploy" "deploy": "wrangler deploy --env production"
}, },
"author": "", "author": "",
"license": "MIT", "license": "MIT",
+172 -9
View File
@@ -4,26 +4,189 @@
echo "🚀 Setting up Email to RSS service..." echo "🚀 Setting up Email to RSS service..."
# Check if npm and npx are installed
if ! command -v npm &> /dev/null || ! command -v npx &> /dev/null; then
echo "❌ Error: npm and npx are required but not found."
echo "Please install Node.js from https://nodejs.org/en/download/"
exit 1
fi
# Check if wrangler-example.toml exists early
if [ ! -f "wrangler-example.toml" ]; then
echo "❌ Error: wrangler-example.toml not found."
exit 1
fi
# Install dependencies # Install dependencies
echo "📦 Installing dependencies..." echo "📦 Installing dependencies..."
npm install npm install
# Create KV namespaces # Check if user is logged in to Cloudflare
echo "🔒 Checking Cloudflare authentication..."
if ! npx wrangler whoami &>/dev/null; then
echo "❌ You are not logged in to Cloudflare. Please run:"
echo "npx wrangler login"
echo "After login completes, run this setup script again."
exit 1
fi
echo "✅ Cloudflare authentication verified"
# Function to get KV namespace IDs
get_kv_namespace_ids() {
echo "️ Retrieving KV namespace IDs..."
# Get the complete KV namespace list
local output
output=$(npx wrangler kv:namespace list 2>/dev/null)
if [ $? -ne 0 ]; then
echo "❌ Error listing KV namespaces. Please check your Cloudflare authentication."
return 1
fi
# Try the direct approach first (most reliable)
MAIN_ID=$(echo "$output" | grep -o '"id": *"[^"]*"' | head -1 | cut -d'"' -f4)
PREVIEW_ID=$(echo "$output" | grep -o '"id": *"[^"]*"' | head -2 | tail -1 | cut -d'"' -f4)
# If the direct approach failed, try to match by namespace title
if [ -z "$MAIN_ID" ] || [ -z "$PREVIEW_ID" ]; then
# Save the output to a file for more complex processing
local temp_file=$(mktemp)
echo "$output" > "$temp_file"
# Try with different patterns
if [ -z "$MAIN_ID" ]; then
MAIN_ID=$(grep -A3 "email-to-rss-EMAIL_STORAGE\"" "$temp_file" | grep -o '"id": "[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "$MAIN_ID" ]; then
MAIN_ID=$(grep -A3 "email-to-rss-EMAIL_STORAGE\"" "$temp_file" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
fi
fi
if [ -z "$PREVIEW_ID" ]; then
PREVIEW_ID=$(grep -A3 "email-to-rss-EMAIL_STORAGE_preview\"" "$temp_file" | grep -o '"id": "[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "$PREVIEW_ID" ]; then
PREVIEW_ID=$(grep -A3 "email-to-rss-EMAIL_STORAGE_preview\"" "$temp_file" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
fi
fi
# Clean up
rm -f "$temp_file"
fi
# Check if we found both IDs
if [ -z "$MAIN_ID" ] || [ -z "$PREVIEW_ID" ]; then
echo "❌ Failed to extract KV namespace IDs. Please run manually:"
echo "npx wrangler kv:namespace list"
echo "And update the IDs in wrangler.toml"
return 1
fi
return 0
}
# Create KV namespaces (suppressing output)
echo "🗄️ Creating KV namespaces..." echo "🗄️ Creating KV namespaces..."
echo "You'll need to update wrangler.toml with these IDs." npx wrangler kv:namespace create EMAIL_STORAGE > /dev/null 2>&1 || true
npx wrangler kv:namespace create EMAIL_STORAGE npx wrangler kv:namespace create EMAIL_STORAGE --preview > /dev/null 2>&1 || true
npx wrangler kv:namespace create EMAIL_STORAGE --preview
# Get KV namespace IDs
get_kv_namespace_ids
if [ $? -ne 0 ]; then
echo "⚠️ Will continue without KV namespace IDs"
KV_ID=""
KV_PREVIEW_ID=""
else
KV_ID="$MAIN_ID"
KV_PREVIEW_ID="$PREVIEW_ID"
fi
# Summarize KV namespace status
echo "📊 KV Namespace Status:"
if [ -z "$KV_ID" ]; then
echo " ❌ Main KV namespace ID: Not found"
SETUP_SUCCESS=false
else
echo " ✅ Main KV namespace ID: $KV_ID"
SETUP_SUCCESS=true
fi
if [ -z "$KV_PREVIEW_ID" ]; then
echo " ❌ Preview KV namespace ID: Not found"
SETUP_SUCCESS=false
else
echo " ✅ Preview KV namespace ID: $KV_PREVIEW_ID"
fi
# Set up admin password # Set up admin password
echo "🔐 Setting up admin password..." echo "🔐 Setting up admin password..."
read -p "Enter admin password: " admin_password read -p "Enter admin password: " admin_password
echo "$admin_password" | npx wrangler secret put ADMIN_PASSWORD
echo "Setting admin password for production environment..."
# Initialize SETUP_SUCCESS if not already set
if [ -z "$SETUP_SUCCESS" ]; then
SETUP_SUCCESS=true
fi
# Try to set the secret without redirecting stderr to see any errors
if [ -z "$admin_password" ]; then
echo "⚠️ No admin password provided. Skipping secret creation."
else
# Run the command and capture its output
SECRET_OUTPUT=$(echo "$admin_password" | npx wrangler secret put ADMIN_PASSWORD --env production --name email-to-rss 2>&1)
SECRET_STATUS=$?
if [ $SECRET_STATUS -ne 0 ]; then
echo "⚠️ Failed to set admin password for production environment"
echo "Error: $SECRET_OUTPUT"
SETUP_SUCCESS=false
else
echo "✅ Admin password set for production environment"
fi
fi
# Prompt for domain # Prompt for domain
read -p "Enter your domain (e.g., yourdomain.com): " domain read -p "Enter your domain (e.g., yourdomain.com): " domain
echo "📝 Please update your domain in wrangler.toml" if [ -z "$domain" ]; then
echo "❌ No domain provided. Cannot continue."
SETUP_SUCCESS=false
else
echo "✅ Domain: $domain"
fi
# Create and update wrangler.toml only if everything is successful
if [ "$SETUP_SUCCESS" = false ]; then
echo "⚠️ Some parts of the setup failed. Will not create wrangler.toml."
echo "Please fix the issues and run the script again."
exit 1
fi
# Create and configure wrangler.toml
echo "📝 Creating and configuring wrangler.toml..."
cp wrangler-example.toml wrangler.toml
# Update wrangler.toml with domain and KV IDs
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS requires empty string for -i
sed -i '' "s/REPLACE_WITH_YOUR_DOMAIN/$domain/g" wrangler.toml
if [ ! -z "$KV_ID" ]; then
sed -i '' "s/REPLACE_WITH_YOUR_KV_NAMESPACE_ID/$KV_ID/g" wrangler.toml
fi
if [ ! -z "$KV_PREVIEW_ID" ]; then
sed -i '' "s/REPLACE_WITH_YOUR_PREVIEW_KV_NAMESPACE_ID/$KV_PREVIEW_ID/g" wrangler.toml
fi
else
# Linux and others
sed -i "s/REPLACE_WITH_YOUR_DOMAIN/$domain/g" wrangler.toml
if [ ! -z "$KV_ID" ]; then
sed -i "s/REPLACE_WITH_YOUR_KV_NAMESPACE_ID/$KV_ID/g" wrangler.toml
fi
if [ ! -z "$KV_PREVIEW_ID" ]; then
sed -i "s/REPLACE_WITH_YOUR_PREVIEW_KV_NAMESPACE_ID/$KV_PREVIEW_ID/g" wrangler.toml
fi
fi
echo "✅ wrangler.toml has been created and configured successfully!"
echo ""
echo "✅ Setup complete! Next steps:" echo "✅ Setup complete! Next steps:"
echo "1. Update wrangler.toml with your KV namespace IDs and domain" echo "1. Set up MX records for your domain with ForwardEmail.net (see README for more details)"
echo "2. Set up MX records for your domain with ForwardEmail.net" echo "2. Deploy with 'npm run deploy'"
echo "3. Deploy with 'npm run deploy'"
+33
View File
@@ -0,0 +1,33 @@
name = "email-to-rss"
main = "src/index.ts"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]
# Global KV Namespace bindings
kv_namespaces = [
{ binding = "EMAIL_STORAGE", id = "REPLACE_WITH_YOUR_KV_NAMESPACE_ID", preview_id = "REPLACE_WITH_YOUR_PREVIEW_KV_NAMESPACE_ID" }
]
# Global Environment variables
[vars]
DOMAIN = "REPLACE_WITH_YOUR_DOMAIN" # Your custom domain for emails
# Development environment
[env.dev]
workers_dev = true
# Production environment
[env.production]
workers_dev = false
kv_namespaces = [
{ binding = "EMAIL_STORAGE", id = "REPLACE_WITH_YOUR_KV_NAMESPACE_ID" }
]
routes = [
{ pattern = "REPLACE_WITH_YOUR_DOMAIN", custom_domain = true },
{ pattern = "www.REPLACE_WITH_YOUR_DOMAIN", custom_domain = true }
]
[env.production.vars]
DOMAIN = "REPLACE_WITH_YOUR_DOMAIN"
-28
View File
@@ -1,28 +0,0 @@
name = "email-to-rss"
main = "src/index.ts"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]
# KV Namespace bindings
kv_namespaces = [
{ binding = "EMAIL_STORAGE", id = "721e2789af9a41eba56a77e7891fd85a", preview_id = "b741d3713dd0416ca80b34ae6539736e" }
]
# Environment variables
[vars]
ADMIN_PASSWORD = "" # Set this using wrangler secret
DOMAIN = "getmynews.app" # Your custom domain for emails
# Development environment
[env.dev]
# Add any development-specific configuration here
workers_dev = true
# Production environment
[env.production]
# Add any production-specific configuration here
workers_dev = false
routes = [
"https://getmynews.app/*",
"https://www.getmynews.app/*"
]