
RSS Digest Email Script You Can Finish. A Build Next Stack field guide for learners shipping small projects.
Scope: five feeds, one email, cron-friendly
Build a script that fetches up to five RSS or Atom feeds, collects entries from the last twenty-four hours, deduplicates by URL, and sends one HTML email summary. No web UI, no database—stateless except optional cache file to avoid duplicate sends. Language: Python with feedparser and smtplib, or Node with equivalent libraries.
Run manually first; then cron at 7 a.m. Success is inbox delivery you actually read three mornings in a row. The stack lesson combines HTTP fetching, XML parsing resilience, HTML email formatting, and secrets via environment variables—not a flashy dashboard.
Pick feeds with different update cadences—daily news, weekly blog, occasional comic. Stress-tests your date filter better than five identical tech blogs updating hourly.
Milestone one: fetch and parse without crashing
Config file feeds.yaml listing name and URL. For each feed:
- Fetch with timeout ten seconds and user-agent identifying your project.
- Parse with library tolerant of messy XML.
- Extract title, link, published date, one-line summary or first 160 chars of content.
- Skip entries older than cutoff or without parseable date—log skip reason.
On fetch failure, record feed name in errors list but continue others. Exit code 0 if at least one feed succeeded; 1 if all failed—cron can alert you. Acceptance: run against five real feeds (news, blog, comic, whatever you read); stdout prints counts per feed.
Cache last-seen URLs in a JSON file with ISO timestamp. On rerun, skip URLs already mailed unless –force flag set for debugging. Idempotent sends prevent duplicate headlines when cron fires twice because laptop woke early.
Milestone two: HTML email that does not look broken
Build email body as inline HTML table or simple sections per feed—no external CSS, no JavaScript. Each entry: linked title, gray timestamp, optional summary paragraph. Plain-text alternative part mandatory; multipart email improves deliverability.
Subject line: Daily digest — 12 items — 2026-06-21. From address must match SMTP auth domain if using transactional provider. Test with yourself CC only before widening recipients.
Secrets: SMTP_HOST, SMTP_USER, SMTP_PASS, MAIL_TO from env—never commit. Provide .env.example with blank values. Acceptance: email renders readable in Gmail mobile and desktop without horizontal scroll.
Send a test email to yourself with –test flag using fixed fixture feeds checked into repo. CI can run dry-run and test modes without secrets; production cron uses real env on the server only.
Acceptance checks before scheduling
Complete before adding cron:
- Duplicate run same day does not duplicate stories if cache file records sent URLs.
- Empty day sends email stating No new items or skips send—pick policy, document it.
- Feed with zero entries in window does not crash layout.
- Malformed XML in one feed does not block others.
- Script runtime under sixty seconds for five feeds on home Wi-Fi.
Log to stdout with ISO timestamps; redirect cron output to a log file you rotate weekly. Silent cron failures are the main production enemy.
Include run duration in log footer. Feeds that suddenly take thirty seconds instead of three often mean a publisher changed CDN or added huge enclosure payloads—duration trend beats guessing.
Template tweaks that improve readability
Group entries under feed name as H2-style bold rows, not nested tables—email clients mangle tables. Limit six entries per feed in v1; link and N more on site if overflow. Overflow handling prevents novel-length emails on busy news days.
Include total count in first paragraph: 12 items from 4 feeds. Scanning one number beats counting headings half awake.
Footer line with unsubscribe instructions even for personal scripts—if you later add a second recipient, habits already exist. For solo use, footer can say Sent by rss-digest on hostname; reply does nothing.
Traps with feed formats and rate limits
Trap: assuming RFC-perfect feeds. Real feeds omit timezones, duplicate GUIDs, and use HTML in titles. Strip tags defensively.
Trap: polling every hour. Daily is enough; respect Cache-Control and do not hammer small blogs.
Trap: embedding images. Block external images in email clients anyway; links only.
Trap: Gmail SMTP from random VPS IP. Use a provider with authenticated SMTP (SendGrid free tier, Mailgun trial) or your workspace relay.
Trap: timezone math by hand. Use UTC internally; convert to local only in display strings with explicit zone in config.
Trap: HTML-only email. Some clients strip HTML; multipart alternative with plain-text duplicate is non-negotiable for reliability.
Running it on a laptop vs a five-dollar VPS
Laptop cron works if machine sleeps—often it does not. For reliable morning digests, a tiny VPS or GitHub Actions scheduled workflow fits. Actions teach CI scheduling; VPS teaches systemd timer and env files in /etc.
Document two install paths in README: local cron and server systemd. Include idempotent install script that checks Python version and installs deps from requirements.txt pin file.
Health check: optional –dry-run prints HTML to stdout without sending—use before editing template styling. Dry-run belongs in every batch script you write after this one.
Pin dependency versions in requirements.txt after first green send. Unpinned feedparser or mail libraries breaking on upgrade at 7 a.m. is a rite of passage you can skip with one lockfile commit.
Subscribe to feeds you already open manually
Replace five bookmarks you check every morning with one email. If the digest is worse than tabs, fix titles and ordering before adding feeds. Finish means you stopped opening three sites separately—not that the HTML won a design award.
After seven days, review skip logs for feeds that always error; remove or fix them. Maintenance is part of the system; a script you ignore is failed automation.
Keep the codebase under two hundred lines if possible. Digest scripts should be readable in one sitting when SMTP auth breaks at 7:02 a.m.
When a feed moves from RSS to Atom, fix the parser once and note the date in README. Feed URLs change; maintenance is part of the skill.