APCUG Workshops Forwarder
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Brendan Kidwell 5c40022fc6 Split apcug_forward.py into the apcug_workshops package
Move the code into one module per concern (config, feed, html_lines,
processing, eastern_time, sending, state, cli). apcug_forward.py stays
as a thin entry point so the cron command and usage are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 01:34:08 -04:00
apcug_workshops Split apcug_forward.py into the apcug_workshops package 2026-10-29 01:34:08 -04:00
doc Move NYACC preamble to the top of forwarded messages 2026-10-29 01:04:38 -04:00
tests Split apcug_forward.py into the apcug_workshops package 2026-10-29 01:34:08 -04:00
.gitignore Add APCUG workshops RSS-to-email forwarder 2026-10-29 00:20:12 -04:00
apcug_forward.py Split apcug_forward.py into the apcug_workshops package 2026-10-29 01:34:08 -04:00
config.example.toml preamble text cleanup 2026-10-29 01:32:50 -04:00
config.local.toml preamble text cleanup 2026-10-29 01:32:50 -04:00
pyproject.toml Add APCUG workshops RSS-to-email forwarder 2026-10-29 00:20:12 -04:00
README.md Split apcug_forward.py into the apcug_workshops package 2026-10-29 01:34:08 -04:00
uv.lock Add APCUG workshops RSS-to-email forwarder 2026-10-29 00:20:12 -04:00

APCUG Workshops Forwarder

Polls APCUG's Wednesday Workshop Mailchimp RSS feed and forwards new announcements by email (in production, to NYACC's Groups.io group). See doc/requirements.md for the full requirements.

Every item in the feed is forwarded, whether it's a Wednesday Workshop or another APCUG announcement such as a Saturday Safari. Before forwarding, each message is cleaned up:

  • APCUG's preamble paragraph starting with "Thank you for forwarding this..." is removed.
  • NYACC's own introduction (preamble_replacement) is inserted at the very top of the message, just before the "View this email in your browser" link (preamble_anchor). If that link is missing, it goes at the top of the body.
  • Footer lines about the recipient's subscription are removed: "This email was sent to", "why did I get this? / unsubscribe" (Wednesday Workshop template), and "Want to change how you receive these emails? / You can update your preferences or unsubscribe" (Saturday Safari template). APCUG's copyright and mailing address lines are kept. The list is footer_remove_prefixes in the config.
  • Unfilled Mailchimp placeholders such as ?e=[UNIQID] are stripped from link URLs (e.g. "View this email in your browser").

Setup

uv sync
cp config.example.toml config.toml   # then edit: addresses and FastMail app password

config.local.toml is preconfigured for testing against the local Mailpit (localhost:1025, no auth).

Usage

uv run apcug_forward.py [--conf PATH] cron
uv run apcug_forward.py [--conf PATH] [--back N] latest
Option Meaning
cron Forward every feed item not seen before, oldest first.
latest Unconditionally forward the newest item.
--back N With latest, forward the item N places older (1 = second-newest).
--conf PATH Configuration file (default config.toml in the project root).
--send-mode smtp|file Override the config's send_mode for this run.
-v Debug logging.

Exit status is 0 on success (including "nothing new"), 1 if fetching or sending failed, 2 for usage or configuration errors. In cron mode the script prints nothing unless it forwards something or hits an error, so cron only mails you when there is news.

Cron

The feed is cached by Mailchimp for 15 minutes, so polling hourly is plenty:

0 * * * * cd /home/brendan/projects/apcug-workshops && /home/brendan/.local/bin/uv run --frozen apcug_forward.py cron

Files

Path Contents
apcug_forward.py Entry point; the code lives in apcug_workshops/.
apcug_workshops/ config (TOML loading), feed (fetch and parse RSS), html_lines (HTML as visual lines), processing (message cleanup), eastern_time (time-zone annotation), sending (build and deliver email), state (seen items), cli (arguments, locking, cron/latest).
cache/ Cached copy of the feed. Its modification time is sent as If-Modified-Since; on a 304 Not Modified the cached copy is used.
state/ state_file from the config: JSON record of every item seen, and a lock file that prevents overlapping runs.
log/ In file send mode, each message is written here as an .eml file instead of being sent.

First run

When the state file doesn't exist yet, every item currently in the feed is recorded as already seen without being forwarded, so the first cron run doesn't flood the group with old announcements. To send the current announcement right away, run latest once after that.

Each config should use its own state_file (the local test config uses state/seen-local.json) so test runs don't mark items as seen for production.

Development

uv run ruff check . && uv run ruff format --check .
uv run pytest