[RELEASE] Email Rate Limit Extension v2 for ISPConfig 3.3 — Rspamd native, no daemon

Discussion in 'Plugins/Modules/Addons' started by Donno, Jun 24, 2026 at 1:38 PM.

  1. Donno

    Donno Member

    Email Rate Limit Extension v2 for ISPConfig 3.3 — Rspamd-native, no daemon

    A rebuild of the v1 email rate limit extension. v2 moves enforcement inside Rspamd — the filter stack ISPConfig 3.3 already ships — and retires the standalone Python policy daemon. The GUI stays the same; the daemon, the /etc/postfix/main.cf edit, and the never-scheduled cron are gone.

    This rebuild came out of @till's critique of v1: Rspamd already has rate limiting built in, so a custom daemon is unnecessary, heavier, and riskier than "a GUI form that writes the matching config files." v2 takes that fully on board — enforcement is a small custom Lua prefilter running inside Rspamd, and the GUI writes the matching config files. No daemon, no custom TCP port, no check_policy_service wiring, no main.cf edits.

    What it does

    - Per-mailbox limit (opt-in): 50/day default, configurable per mailbox + a server-wide default. Only mailboxes with the Rate Limit tab checkbox ON are capped; unchecked mailboxes have no per-user cap (only the global limit applies).
    - Global (server-wide) limit: 999/day default, configurable.
    - Per-recipient counting: 1 message × N recipients = N against the cap (To + CC + BCC all count). A 3-recipient send counts as 3.
    - Hard midnight reset: counters are Redis keys erl:YYYY-MM-DD:<email> and erl:YYYY-MM-DD:global. A new day = new keys = automatic reset. Redis TTL ~26h cleans old days. No cron.
    - Only accepted messages count: a two-key atomic Redis script checks the limit before incrementing either counter. An over-limit message increments neither — a rejected burst burns no quota on either counter.
    - Fail-open: if Redis is unreachable or the Lua module errors, mail is allowed (never blocked by a counter outage).
    - Defer, not reject: over-limit → 4xx soft-reject at SMTP time (the client retries; nothing is lost).
    - Unauthenticated mail is not counted (no SASL user → skipped).
    - Distinct SMTP errors: the 4xx names the sender + the actual limit hit ("Your daily limit of N messages reached" for a mailbox cap, "Global daily limit of N reached" for the server cap).
    - Recent Violations panel: each over-limit attempt is logged to a Redis list (erl:violations) with timestamp, type, user, limit, count, and client IP; the admin panel reads it (newest first, last 24h, capped at 200).

    How it differs from v1

    Aspect | v1 | v2
    Enforcement | Python Postfix policy daemon (TCP 10040) | Rspamd Lua prefilter + Redis
    /etc/postfix/main.cf | edited (mua_sender_restrictions = check_policy_service …) | never touched
    Daily reset | a cron job (never properly scheduled → broken) | date-keyed Redis keys (automatic)
    Per-mailbox counter / "sent today" | MySQL er_ratelimit_history | Redis erl:DATE:<email>
    Violations audit | MySQL er_ratelimit_violations | Redis list erl:violations (Recent Violations panel)
    Per-recipient counting | per message (not per recipient) | per recipient
    Rejected burst | could burn quota | burns nothing (atomic check-then-increment)
    GUI tabs | per-mailbox + admin | same (kept)

    Same GUI; the daemon + main.cf edit + broken cron are retired.

    Requirements

    - ISPConfig 3.3+ with content_filter = rspamd and Redis (the standard 3.3 setup — Rspamd + Redis are already configured, nothing to add).
    - Single Redis (Redis Cluster is not supported — the two-key atomic script needs both keys in the same hash slot).

    Install (staged — mail never stops)

    chmod +x install_email_ratelimit_v2.sh
    ./install_email_ratelimit_v2.sh # install — log-only: counts, never defers
    # observe first (zero risk — nothing is enforced yet):
    redis-cli get "erl:$(date +%F):global"
    tail -f /var/log/rspamd/rspamd.log | grep erl # "erl ok" / "erl DEFER"
    ./install_email_ratelimit_v2.sh enforce # flip to deferring (4xx over-limit)

    Enable per-mailbox caps in Mail → Mailbox → Rate Limit. Set the global limit + the default per-mailbox limit in System → Server Config → Email Rate Limit. A 1-minute sync cron keeps the Rspamd config + per-user map in sync with GUI saves (idempotent; no-op when already in sync — it's a config-sync cron, not the v1 policy daemon).

    Migrating from v1

    v2 reuses v1's er_mail_user_ratelimit + er_global_ratelimit tables, so your existing per-mailbox limits carry over. Remove only v1's enforcement layer (the daemon + main.cf line + cron), keep the tables, then install v2. The README's "Migrating from v1" section has the exact paths + commands.

    If you already fully uninstalled v1 (which DROPs all er_* tables), the per-mailbox limits are gone — just install v2 and re-enter them in the GUI.

    Verified live

    Per-recipient counting (a 3-recipient send → counter +3), whole-message defer (an over-limit multi-recipient send is refused — none delivered), quota preservation (the rejected burst increments nothing), per-user + global violations in the Recent Violations panel, distinct SMTP messages naming the sender + the limit. Fail-open is by design (every Redis/Lua call is pcall-wrapped) — verify it on your box with systemctl stop redis-server → send a test → must be accepted → systemctl start redis-server. Developed + validated on a live ISPConfig 3.3.0p3 / Rspamd 3.8.1 / Debian 12 server.

    Limitations

    - Single Redis only (no Cluster).
    - Recent Violations is per-server (each mail server logs to its own Redis); on multiserver the master interface sees only the master's violations.
    - IDN: works as long as mail_user.email (stored punycode) matches task:get_user() (SASL uses the stored email) — verify if you host IDN domains.

    Rollback

    ./install_email_ratelimit_v2.sh disable # deactivate (keep files)
    ./install_email_ratelimit_v2.sh uninstall # remove files (DB tables + Redis counters left)

    No main.cf restore is needed — v2 never edited it. Clear the Redis counters if you want a clean slate: redis-cli --scan --pattern 'erl:*' | xargs -r redis-cli del.

    Target: ISPConfig 3.3.0p3, Rspamd 3.8.1, Debian 12.

    Developed with the assistance of GLM-5.2 (AI coding assistant).
     

    Attached Files:

Share This Page