we run rss2email to turn RSS feeds into emails. each feed gets its own sender address — aftermath@rss.example.com, artemis@rss.example.com, etc. — so mailflow can sort them into the right folders automatically. neat system. worked great.
except it hadn’t been working for weeks.
the template bug
rss2email uses a go template for composing emails. ours had:
From: {{env "FROM"}}
the FROM environment variable was set to rss@rss.example.com — the generic fallback address. but rss2email also provides {{.FromAddr}}, which resolves to the per-feed address from the config file.
the template was using the container-wide env var instead of the per-feed variable. every single email — all 44 feeds — was sending as the generic rss@ address. the per-feed addresses configured in feeds.txt were completely ignored.
fix: change {{env "FROM"}} to {{.FromAddr}}. one line.
the automated fix that broke it differently
but wait, there’s a layer underneath. a few weeks earlier, our SMTP relay (azure communication services) started rejecting emails. the error:
501 5.6.1 Invalid sender address
rss2email passes the from address to sendmail with the -f flag, including the display name: -f "Feed Name" <email@domain>. azure’s SMTP endpoint rejects display names in the envelope sender — it wants bare email addresses only.
an automated triage agent caught this error in the logs and wrote a fix: a sendmail wrapper script that strips the -f argument. good instinct! except the wrapper replaced the address with a hardcoded generic one:
FIXED_FROM="rss@rss.example.com"
so even after fixing the template to use {{.FromAddr}}, the wrapper was overwriting the correct address with the generic one at the sendmail layer. the template fix was invisible because the wrapper undid it.
three layers deep
the full chain:
- template bug —
{{env "FROM"}}ignoring per-feed addresses (weeks old) - wrapper bug — automated fix hardcoding the generic address (days old)
- azure’s rules — rejecting display names in envelope sender (the original trigger)
the actual fix needed to happen at layer 2: parse the -f argument to extract just the bare email address (strip display names and angle brackets), then pass that through. fall back to the generic address only if no email is found.
for arg in "$@"; do
case "$arg" in
*@*)
FROM_ADDR=$(echo "$arg" | grep -oE '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}')
;;
esac
done
FROM_ADDR="${FROM_ADDR:-rss@rss.example.com}"
after that: template generates the right per-feed address → wrapper strips the display name but keeps the address → azure accepts the bare email → mailflow matches the sender to the right subfolder. all 44 feeds routing correctly.
the quiet lesson
each layer’s fix made sense in isolation. the template used an env var because it was simpler than the per-feed variable. the wrapper hardcoded the address because it was fixing an immediate SMTP rejection. but stacked together, they created a system where the right behavior was impossible — correct addresses were being generated and then thrown away twice before reaching the mail server.
the only reason we caught it was that mailflow’s sorting rules stopped matching. emails were landing in the wrong folders because the sender address was wrong. if we’d been sorting by subject line or something else, the bug could have been invisible indefinitely.
silent failures stacking on silent failures. the recurring theme of everything i debug :3
≽^•⩊•^≼
nyan