https://github.com/nbari/policyd-rate-limit
Postfix rate limiter SMTP policy daemon
https://github.com/nbari/policyd-rate-limit
limit postfix postfix-policy-server quota rate rate-limit rate-limiting smtp
Last synced: 4 months ago
JSON representation
Postfix rate limiter SMTP policy daemon
- Host: GitHub
- URL: https://github.com/nbari/policyd-rate-limit
- Owner: nbari
- License: bsd-3-clause
- Created: 2020-02-03T12:47:29.000Z (over 6 years ago)
- Default Branch: main
- Last Pushed: 2025-12-31T15:50:04.000Z (5 months ago)
- Last Synced: 2026-01-04T20:33:04.869Z (5 months ago)
- Topics: limit, postfix, postfix-policy-server, quota, rate, rate-limit, rate-limiting, smtp
- Language: Rust
- Homepage:
- Size: 170 KB
- Stars: 5
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README
# policyd-rate-limit
[](https://crates.io/crates/policyd-rate-limit)
[](https://github.com/nbari/policyd-rate-limit/actions/workflows/test.yml)
Postfix rate limiter SMTP policy daemon
# How it works
It depends on the [Postfix policy delegation protocol](http://www.postfix.org/SMTPD_POLICY_README.html), it searches for the `sasl_username` and based on the defined limits stored in a SQL(MySQL/PostgreSQL/SQLite) database it rejects or allows `action=DUNNO` the email to be sent.
```mermaid
flowchart TD
A[Policy request] --> B{Has sasl_username?}
B -- No --> C[action=DUNNO]
B -- Yes --> D[Fetch rate windows from DB]
D --> E{User exists?}
E -- No --> F[Create rate windows for user]
F --> C
E -- Yes --> G[Reset expired windows]
G --> H{All windows within quota?}
H -- Yes --> I[action=DUNNO]
H -- No --> J[action=REJECT]
I --> K[Increment used counters]
J --> K
```
# How to use
```txt
Postfix policy daemon for rate limiting
Usage: policyd-rate-limit [OPTIONS] --dsn
Options:
-s, --socket Path to the Unix domain socket [default: /tmp/policy-rate-limit.sock]
--dsn Database connection string [env: DSN=]
--pool Pool size for database connections [default: 5]
-l, --limit Maximum allowed messages per rate window (repeatable, default: 10)
-r, --rate rate in seconds for each window (repeatable, default: 86400)
-v, --verbose... Increase verbosity, -vv for debug
-h, --help Print help
-V, --version Print version
```
Repeat `--limit` and `--rate` to configure multiple windows, for example:
```
policyd-rate-limit --dsn ... -l 7 -r 3600 -l 100 -r 86400
```
All configured windows are enforced together: a request is allowed only when *every* window
is still under quota. This means the most restrictive window effectively caps traffic.
## Migration notes (1.1.0+)
The `ratelimit` table now uses a composite primary key `(username, rate)` to support multiple
windows per user. Migrate existing tables as follows:
Postgres:
```sql
ALTER TABLE ratelimit ALTER COLUMN rate SET NOT NULL;
ALTER TABLE ratelimit DROP CONSTRAINT ratelimit_pkey;
ALTER TABLE ratelimit ADD PRIMARY KEY (username, rate);
```
MariaDB/MySQL:
```sql
ALTER TABLE ratelimit MODIFY rate INT UNSIGNED NOT NULL DEFAULT 0;
ALTER TABLE ratelimit DROP PRIMARY KEY;
ALTER TABLE ratelimit ADD PRIMARY KEY (username, rate);
```
SQLite (recreate table):
```sql
CREATE TABLE ratelimit_new (
username TEXT NOT NULL,
quota INTEGER NOT NULL DEFAULT 0,
used INTEGER NOT NULL DEFAULT 0,
rate INTEGER NOT NULL DEFAULT 0,
rdate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (username, rate)
);
INSERT INTO ratelimit_new (username, quota, used, rate, rdate)
SELECT username, quota, used, rate, rdate FROM ratelimit;
DROP TABLE ratelimit;
ALTER TABLE ratelimit_new RENAME TO ratelimit;
```
The database schema (postgres example, one row per rate window):
```sql
CREATE TABLE IF NOT EXISTS ratelimit (
username VARCHAR(128) NOT NULL, -- sender address (SASL username)
quota INTEGER NOT NULL DEFAULT 0, -- limit
used INTEGER NOT NULL DEFAULT 0, -- current recipient counter
rate INTEGER NOT NULL DEFAULT 0, -- seconds after which the counter gets reset
rdate TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- datetime when counter was reset
PRIMARY KEY (username, rate)
);
```
# Postfix configuration
Add the path of the policy-rate-limit socket to `smtpd_sender_restrictions` for example:
smtpd_sender_restrictions: check_policy_service { unix:/tmp/policy-rate-limit.sock, default_action=DUNNO }
> check the perms of the socket