Send email alerts or digests when your RSS/Atom feeds update.
- Feed Grouping & Deduplication: Combine multiple feed URLs into a single group. Items from all feeds in a group are deduplicated and processed together.
- Digest or Individual Emails: Choose between sending a single digest email for multiple updates or separate emails for each new item. Automatic digesting if too many updates.
- Flexible Per-Feed Settings: Settings (recipients, templates, update keys, etc.) can be set globally or overridden for each feed group.
- Custom Update Keys: Detect updates using traditional GUIDs or any custom content via MiniJinja expressions, allowing notification on any change you care about.
- Customizable Email Templates: Use MiniJinja templates for email subject and body.
- Advanced Filtering: Filter feed items using logical combinations (
and/all,or/any,not), regular expressions, or MiniJinja expressions for fine-grained control. - Notification On Error: Send notifications when feeds are not working.
- HTML Sanitization: Sanitize feed HTML content for safer emails.
- Write a config file with your feeds.
- Set the environment variables described in the next section.
- Start the service with Docker Compose:
docker compose up -d(seedocker-compose.yml).
YAF2M_CONFIG_PATH: path to the config file (default:config/config.toml).POSTGRES_URL: database connection string; see sqlx::postgres::PgConnectOptions.SMTP_FROM: sender address, e.g."yaf2m" <[email protected]>.SMTP_URL: SMTP transport URL; see lettre::transport::smtp::SmtpTransport::from_url.
Note: The config file is auto-reloaded. There is no need to restart the service.
You can use opml-to-config.py to convert an OPML file to yaf2m config.
Minimal:
[settings]
to = '[email protected]'
[[feeds]]
url = 'https://example.com/feed.xml'
[[feeds]]
url = 'https://example.org/feed.atom'With default values:
error-report-to = [] # error-report-to = "[email protected]"
[settings]
to = []
cc = []
bcc = []
digest = false
max-mails-per-check = 5
item-subject = <src/templates/item-subject.txt>
digest-subject = <src/templates/digest-subject.txt>
item-body = <src/templates/item-body.html>
digest-body = <src/templates/digest-body.html>
template-args = {}
update-key = 'item.id'
interval = '1h'
keep-old = '1w'
timeout = '30s'
sanitize = true
sort-by-last-modified = false
http-headers = {}
[[feeds]]
url = "https://blog.rust-lang.org/feed.xml"
# urls = ["https://example.org/feed.atom", "https://example.net/feed.json"]
# To override [settings]:
# to = ["Alice <[email protected]>", "[email protected]"]
# cc = "[email protected]" is the same as cc = ["[email protected]"]
# bcc = []
# digest = true
# max-mails-per-check = 1
# item-subject.inline = "{{ item.title.content }}"
# digest-subject.inline = "My daily feed on {{ now() | dateformat(tz=template_args.tz) }}"
# item-body.file = "/path/to/item-template.html"
# digest-body.file = "/path/to/item-template.html"
# template-args.tz = "Asia/Shanghai"
# update-keys = ['item.title', 'item.content | capture("<main>([\\s\\S]*?)</main>", 1)']
# interval = '1d'
# keep-old = '2w'
# timeout = '1m'
# sanitize = false
# sort-by-last-modified = true
# http-headers.user-agent = "xxx"
feeds.filter.any = [
{ title-regex = '^Announcing' },
{
all = [
{ not.body-regex = 'foo' },
{ jinja-expr = 'item.author.name is match("John")' },
],
},
]- Feeds are organized as groups (
[[feeds]]). One group may contain one or more feed URLs. Feeds in the same group are combined together and items are deduplicated. urlsandfilterare group-specific. Other settings may have a global default value in[settings]. Settings resolve in order: value on the feed group -> value in[settings]-> built-in default.
to,cc,bcc: Mail recipients. Each can be a single string or an array of strings.digest: Whether to send all updates in a single digest mail or to send one mail per item. Newly added feeds and updates triggered by configuration changes (e.g.update-keysorfilter) are always sent in digests.max-mails-per-check: Send digest if there are too many updates, even ifdigest = false.item-subject,digest-subject,item-body,digest-body: MiniJinja templates for mail contents.- Can be
{ inline = "{{ template }}" }or{ file = "/path/to/template" }. - Default templates:
src/templates. - Context for single item:
{ feed => Feed, item => Entry }, seefeed_rs::model::Feedandfeed_rs::model::Entry. - Context for digest:
{ feeds => [Feed], items => [{ feed => Feed, item => Entry }] }, wherefeedsare all feeds in the group (no matter updated or not), anditemsare updated items. - Custom args:
template-args. - Can include each other, e.g.
{% include "item-body.html" %},{% include "digest-subject.txt" %}. - More features:
- builtin
filtersandtests minijinja-contribfiltersandglobals- Regular expressions:
str is match(regex),str | capture(regex[, group]),str | regex_replace(regex, replacement).
- builtin
- Can be
template-args: Custom args that are passed to the MiniJinja templates. Template args set on each feed are merged with the global setting. Args used by the default templates:tz: timezonegroup_title: used by the defaultdigest-subjecttemplate to display the title for the entire feed group (useful when there are multiple URLs in a feed group)
update-keys/update-key: Keys that are used to check whether a feed item is updated or not. Each key is a MiniJinja expression. This can be used to control whether to notify feed content update.interval: Check feed update once per interval.keep-old: Prune old data in the database.timeout: Timeout when fetching the feed.sanitize: Whether to sanitize HTML in feed contents or keep the HTML as it is.sort-by-last-modified: Whether to sort items in a digest by their last modified time.http-headers: HTTP header map when fetching the feed.
url/urls: Feed URLs in the group.filter: Filter feed items. Can be one of:title-regex/body-regex/regex: Regular expression match for title / body / both.jinja-expr: Evaluated as MiniJinja expression to see if it's true.and: [..](all: [..]) /or: [..](any: [..]) /not: {..}: Logic combination.
error-report-to: Error report recipients when feeds are not working.
- Do not load untrusted config files. The config is designed to be flexible but insecure. Untrusted config may lead to SSTI, DoS attacks, and email bombs. This is out of the threat model for this project.
- See Security for the security policy.