install_postfixadmin.sh: update used_quotas configuration to 'YES' for PostfixAdmin

install_update_dovecot-2.4.sh: add quota_clone plugin configuration for real-time quota tracking
create settings.json: add permissions for quota-related commands in PostfixAdmin
create install_quota_clone.sh: script to configure Dovecot quota_clone plugin and PostfixAdmin integration
This commit is contained in:
2026-06-29 15:12:56 +02:00
parent 163fc55a6d
commit 625184ab6e
4 changed files with 668 additions and 2 deletions
+63
View File
@@ -0,0 +1,63 @@
{
"permissions": {
"allow": [
"Bash(ssh a.mx.oopen.de \"cat /usr/local/dovecot-2.4.2/etc/dovecot/conf.d/90-quota.conf 2>/dev/null || echo 'FILE NOT FOUND'\")",
"Bash(ssh a.mx.oopen.de \"grep -n -i 'quota\\\\|imap' /var/www/adm.oopen.de/postfixadmin-3.3.13/config.local.php 2>/dev/null | head -40\")",
"Bash(ssh a.mx.oopen.de \"grep -n -i 'quota\\\\|getquota\\\\|imap' /var/www/adm.oopen.de/postfixadmin-3.3.13/config.inc.php 2>/dev/null | head -60\")",
"Bash(ssh a.mx.oopen.de \"grep -n -i 'quota\\\\|getquota\\\\|imap\\\\|dovecot_doveadm\\\\|doveadm' /var/www/adm.oopen.de/postfixadmin-3.3.13/config.inc.php 2>/dev/null | grep -v '^.*:\\\\/\\\\/' | head -60\")",
"Bash(ssh a.mx.oopen.de \"sed -n '680,780p' /var/www/adm.oopen.de/postfixadmin-3.3.13/config.inc.php\")",
"Bash(ssh a.mx.oopen.de \"sudo sed -n '680,780p' /var/www/adm.oopen.de/postfixadmin-3.3.13/config.inc.php\")",
"Bash(ssh a.mx.oopen.de \"ls -la /var/www/adm.oopen.de/postfixadmin-3.3.13/ && cat /var/www/adm.oopen.de/postfixadmin-3.3.13/config.local.php 2>/dev/null | grep -n -i 'quota\\\\|imap\\\\|doveadm' | head -40\")",
"Bash(ssh a.mx.oopen.de \"grep -n 'used_quotas\\\\|new_quota_table\\\\|getquota\\\\|imap_quota\\\\|doveadm\\\\|dovecot' /var/www/adm.oopen.de/postfixadmin-3.3.13/config.local.php 2>/dev/null\")",
"Bash(ssh a.mx.oopen.de \"grep -n 'getquota\\\\|quota_get\\\\|imap.*quota\\\\|GETQUOTA\\\\|quota_used\\\\|used_quota\\\\|dovecot_quota' /var/www/adm.oopen.de/postfixadmin-3.3.13/functions.inc.php 2>/dev/null | head -30\")",
"Bash(ssh a.mx.oopen.de \"/usr/local/dovecot-2.4.2/bin/doveadm quota get -u 'ckubu@oopen.de' 2>/dev/null || /usr/local/dovecot/bin/doveadm quota get -u 'ckubu@oopen.de' 2>&1 | head -20\")",
"Bash(ssh a.mx.oopen.de \"/usr/local/dovecot/bin/doveadm quota get -u 'chris@oopen.de' 2>&1 | head -10\")",
"Bash(ssh a.mx.oopen.de \"/usr/local/dovecot/bin/doveadm user '*@oopen.de' 2>&1 | head -5\")",
"Bash(ssh a.mx.oopen.de \"psql -U postfix postfix -c \\\\\"SELECT username, quota FROM mailbox LIMIT 5;\\\\\" 2>/dev/null\")",
"Bash(ssh a.mx.oopen.de \"/usr/local/dovecot/bin/doveadm quota get -u 'bastisch@kottbusserdamm.net' 2>&1\")",
"Bash(ssh a.mx.oopen.de \"cat /usr/local/dovecot-2.4.2/etc/dovecot/conf.d/10-auth.conf 2>/dev/null | grep -v '^#\\\\|^$' | head -30\")",
"Bash(ssh a.mx.oopen.de \"ls /usr/local/dovecot-2.4.2/var/run/dovecot/ 2>/dev/null || ls /var/run/dovecot/ 2>/dev/null | head -20\")",
"Bash(ssh a.mx.oopen.de \"/usr/local/dovecot/bin/doveadm -D quota get -u 'bastisch@kottbusserdamm.net' 2>&1 | tail -20\")",
"Bash(ssh a.mx.oopen.de \"ls /usr/local/dovecot-2.4.2/lib/dovecot/ 2>/dev/null | grep -i quota\")",
"Bash(ssh a.mx.oopen.de \"cat /usr/local/dovecot-2.4.2/etc/dovecot/conf.d/90-quota.conf | grep -v '^#\\\\|^$'\")",
"Bash(ssh a.mx.oopen.de \"cat /usr/local/dovecot-2.4.2/etc/dovecot/dovecot.conf | grep -v '^#\\\\|^$'\")",
"Bash(ssh a.mx.oopen.de \"ls /usr/local/dovecot-2.4.2/etc/dovecot/conf.d/ && cat /usr/local/dovecot-2.4.2/etc/dovecot/conf.d/20-imap.conf 2>/dev/null | grep -v '^#\\\\|^$'\")",
"Bash(ssh a.mx.oopen.de \"cat /usr/local/dovecot-2.4.2/etc/dovecot/conf.d/10-mail.conf | grep -v '^#\\\\|^$'\")",
"Bash(ssh a.mx.oopen.de \"cat /usr/local/dovecot-2.4.2/share/doc/dovecot/quota_clone* 2>/dev/null || find /usr/local/dovecot-2.4.2/share/doc/dovecot/ -name '*quota*' 2>/dev/null | head -10\")",
"Bash(ssh a.mx.oopen.de \"ls /usr/local/dovecot-2.4.2/etc/dovecot/*.conf* 2>/dev/null && find /usr/local/dovecot-2.4.2/etc/dovecot/ -name '*.ext' 2>/dev/null\")",
"Bash(ssh a.mx.oopen.de \"ls /usr/local/dovecot-2.4.2/lib/dovecot/ | grep -i dict\")",
"Bash(ssh a.mx.oopen.de \"cat /usr/local/dovecot-2.4.2/etc/dovecot/sql-connect.conf.ext\")",
"Bash(ssh a.mx.oopen.de \"ls /usr/local/dovecot-2.4.2/lib/dovecot/ | grep -i 'pgsql\\\\|mysql\\\\|sqlite\\\\|db'\")",
"Bash(ssh a.mx.oopen.de \"ls /usr/local/dovecot-2.4.2/lib/ | grep -i 'sql\\\\|dict\\\\|pgsql\\\\|mysql'\")",
"Bash(ssh a.mx.oopen.de \"find /usr/local/dovecot-2.4.2/ -name '*dict*' -o -name '*pgsql*' -o -name '*sql*' 2>/dev/null | grep '\\\\.so'\")",
"Bash(ssh a.mx.oopen.de \"psql -U postfix postfix -c \\\\\"SELECT * FROM quota2 LIMIT 5;\\\\\" 2>/dev/null\")",
"Bash(ssh a.mx.oopen.de \"psql -U postfix postfix -c \\\\\"\\\\dt\\\\\" 2>/dev/null | grep -i quota\")",
"Bash(ssh a.mx.oopen.de \"find /usr/local/dovecot-2.4.2/share/ -name '*.conf' -o -name '*.conf.ext' -o -name '*example*' 2>/dev/null | head -20\")",
"Bash(ssh a.mx.oopen.de \"/usr/local/dovecot-2.4.2/sbin/dovecot -a 2>/dev/null | grep -i 'quota_clone\\\\|clone' | head -20\")",
"Bash(ssh a.mx.oopen.de \"/usr/local/dovecot-2.4.2/sbin/dovecot -a 2>/dev/null | grep -i 'quota_clone' | head -20\")",
"Bash(ssh a.mx.oopen.de \"/usr/local/dovecot-2.4.2/sbin/dovecot -a 2>/dev/null | grep -i 'dict\\\\|clone' | head -40\")",
"Bash(ssh a.mx.oopen.de \"/usr/local/dovecot-2.4.2/sbin/dovecot -a 2>/dev/null | grep -A5 'service dict'\")",
"Bash(ssh a.mx.oopen.de \"cat /usr/local/dovecot-2.4.2/etc/dovecot/conf.d/10-master.conf | grep -v '^#\\\\|^$'\")",
"Bash(ssh a.mx.oopen.de \"grep -n 'imap\\\\|getquota\\\\|quota' /var/www/adm.oopen.de/postfixadmin-3.3.13/functions.inc.php 2>/dev/null | grep -i 'imap.*quota\\\\|quota.*imap\\\\|getquota\\\\|imap_open\\\\|imap_getquota\\\\|used_quota' | head -20\")",
"Bash(ssh a.mx.oopen.de \"grep -n 'getquota\\\\|imap_open\\\\|imap_close\\\\|imap_quota' /var/www/adm.oopen.de/postfixadmin-3.3.13/functions.inc.php 2>/dev/null | head -20\")",
"Bash(ssh a.mx.oopen.de \"cat /usr/local/dovecot-2.4.2/etc/dovecot/conf.d/auth-sql.conf.ext\")",
"Bash(ssh a.mx.oopen.de \"/usr/local/dovecot-2.4.2/bin/doveadm -D quota get -u 'bastisch@kottbusserdamm.net' 2>&1 | grep -E 'Error|Debug.*auth|FAIL|passdb|userdb' | head -15\")",
"Bash(ssh a.mx.oopen.de \"ls /var/www/adm.oopen.de/postfixadmin-3.3.13/ADDITIONS/ && ls /var/www/adm.oopen.de/postfixadmin-3.3.13/scripts/\")",
"Bash(ssh a.mx.oopen.de \"journalctl -u dovecot --since '5 minutes ago' 2>/dev/null | grep -i 'quota\\\\|bastisch\\\\|userdb\\\\|error' | head -20\")",
"Bash(ssh a.mx.oopen.de \"cat /var/www/adm.oopen.de/postfixadmin-3.3.13/ADDITIONS/quota_usage.pl\")",
"Bash(ssh a.mx.oopen.de \"ls -la /var/www/adm.oopen.de/postfixadmin-3.3.13/ADDITIONS/\")",
"Bash(ssh a.mx.oopen.de \"sudo cat /var/www/adm.oopen.de/postfixadmin-3.3.13/ADDITIONS/quota_usage.pl 2>/dev/null || sg www-data -c 'cat /var/www/adm.oopen.de/postfixadmin-3.3.13/ADDITIONS/quota_usage.pl' 2>/dev/null\")",
"Bash(ssh a.mx.oopen.de \"id && groups\")",
"Bash(ssh a.mx.oopen.de \"grep -E 'quota|bastisch|userdb.*FAIL' /var/log/mail.log 2>/dev/null | tail -20\")",
"Bash(ssh a.mx.oopen.de \"/usr/local/dovecot/bin/doveadm -c /usr/local/dovecot-2.4.2/etc/dovecot/dovecot.conf user 'bastisch@kottbusserdamm.net' 2>&1\")",
"Bash(ssh a.mx.oopen.de \"psql -U postfix postfix -c \\\\\"SELECT username, maildir, quota, active FROM mailbox WHERE username = 'bastisch@kottbusserdamm.net';\\\\\" 2>/dev/null\")",
"Bash(ssh a.mx.oopen.de \"grep -i 'auth.*worker\\\\|sql.*error\\\\|error.*sql\\\\|userdb.*error' /var/log/syslog 2>/dev/null | tail -10\")",
"Bash(ssh a.mx.oopen.de \"journalctl -u dovecot --since '10 minutes ago' 2>/dev/null | grep -i 'userdb\\\\|auth.*worker\\\\|sql\\\\|error' | head -20\")",
"Bash(ssh a.mx.oopen.de \"journalctl -t dovecot --since '10 minutes ago' 2>/dev/null | grep -i 'bastisch\\\\|userdb\\\\|doveadm' | head -20\")",
"Bash(ssh a.mx.oopen.de \"cat /usr/local/dovecot-2.4.2/etc/dovecot/conf.d/10-logging.conf | grep -v '^#\\\\|^$'\")",
"Bash(ssh a.mx.oopen.de \"grep -i 'bastisch\\\\|auth.*worker\\\\|userdb.*fail\\\\|doveadm' /var/log/local1.log 2>/dev/null | tail -20 || grep -i 'bastisch\\\\|auth.*worker\\\\|userdb.*fail\\\\|doveadm' /var/log/syslog 2>/dev/null | tail -10\")",
"Bash(ssh a.mx.oopen.de \"find /var/log/ -name '*.log' -newer /var/log/auth.log 2>/dev/null | head -10 && ls /var/log/*.log 2>/dev/null | head -10\")",
"Bash(ssh a.mx.oopen.de \"grep -i 'bastisch\\\\|doveadm.*quota\\\\|auth.*fail\\\\|userdb.*error' /var/log/dovecot/dovecot.log 2>/dev/null | tail -20\")"
]
}
}
+2 -2
View File
@@ -2610,7 +2610,7 @@ fi
## - $CONF['show_undeliverable']='NO';
## - $CONF['show_popimap']='NO';
## -
## - $CONF['used_quotas'] = 'NO';
## - $CONF['used_quotas'] = 'YES';
## - $CONF['new_quota_table'] = 'YES';
## -
echononl "\tAdjust Postfix Admin's Configuration - Part 5"
@@ -2642,7 +2642,7 @@ perl -i -n -p -e "s#^(\s*\\\$CONF\['show_undeliverable'\]\s*=.*)#//!\1\n\\\$CONF
$pfa_conf_file >> $log_file 2>&1 || _failed=true
perl -i -n -p -e "s#^(\s*\\\$CONF\['show_popimap'\]\s*=.*)#//!\1\n\\\$CONF['show_popimap'] = 'NO';#" \
$pfa_conf_file >> $log_file 2>&1 || _failed=true
perl -i -n -p -e "s#^(\s*\\\$CONF\['used_quotas'\]\s*=.*)#//!\1\n\\\$CONF['used_quotas'] = 'NO';#" \
perl -i -n -p -e "s#^(\s*\\\$CONF\['used_quotas'\]\s*=.*)#//!\1\n\\\$CONF['used_quotas'] = 'YES';#" \
$pfa_conf_file >> $log_file 2>&1 || _failed=true
perl -i -n -p -e "s#^(\s*\\\$CONF\['new_quota_table'\]\s*=.*)#//!\1\n\\\$CONF['new_quota_table'] = 'YES';#" \
$pfa_conf_file >> $log_file 2>&1 || _failed=true
+350
View File
@@ -0,0 +1,350 @@
#!/bin/bash
##
## install_quota_clone.sh
##
## Configures Dovecot 2.4 quota_clone plugin and enables PostfixAdmin
## quota display by writing real-time usage to the quota2 PostgreSQL table.
##
## Must be run as root on the target mail server.
##
## What this script does:
## - Backs up the Dovecot and PostfixAdmin directories
## - Drops the old ADD-semantics mergequota2 PostgreSQL trigger
## - Creates a new SET-semantics upsertquota2 trigger (validation only)
## - Creates /etc/dovecot/conf.d/91-quota-clone.conf (dict_server)
## - Appends the quota_clone plugin block to 90-quota.conf
## - Enables $CONF['used_quotas'] and $CONF['new_quota_table'] in PostfixAdmin
## - Installs /usr/local/bin/update_quota2.sh for initial batch fill
## - Reloads Dovecot
##
## Safe to run multiple times (idempotent).
##
set -uo pipefail
# ── paths ─────────────────────────────────────────────────────────────────────
DOVECOT_LINK="/usr/local/dovecot"
PFA_LINK="/var/www/adm.oopen.de/postfixadmin"
BACKUP_BASE="/root/backup"
# ── colour helpers ────────────────────────────────────────────────────────────
_bold='\033[1m'; _green='\033[0;32m'; _red='\033[0;31m'; _yellow='\033[1;33m'; _nc='\033[0m'
heading() { echo; echo -e "${_bold}${*}${_nc}"; }
step() { printf ' %-60s' "$*"; }
ok() { echo -e " [${_green}OK${_nc}]"; }
skip() { echo -e " [${_yellow}SKIP${_nc}]"; }
failed() { echo -e " [${_red}FAILED${_nc}]"; }
info() { echo -e " ${*}"; }
die() { echo -e "\n${_red}FATAL: ${*}${_nc}" >&2; exit 1; }
# ── root check ────────────────────────────────────────────────────────────────
[[ $EUID -eq 0 ]] || die "This script must be run as root."
# ── resolve symlinks ──────────────────────────────────────────────────────────
[[ -L "$DOVECOT_LINK" ]] || die "$DOVECOT_LINK is not a symlink."
[[ -L "$PFA_LINK" ]] || die "$PFA_LINK is not a symlink."
DOVECOT_REAL=$(realpath "$DOVECOT_LINK")
PFA_REAL=$(realpath "$PFA_LINK")
DOVECOT_CONF_D="${DOVECOT_REAL}/etc/dovecot/conf.d"
SQL_CONNECT="${DOVECOT_REAL}/etc/dovecot/sql-connect.conf.ext"
PFA_CONFIG="${PFA_REAL}/config.local.php"
echo
echo -e "${_bold}Dovecot quota_clone + PostfixAdmin quota display${_nc}"
echo
info "Dovecot : $DOVECOT_REAL"
info "PFA : $PFA_REAL"
# ── read DB credentials from Dovecot's sql-connect.conf.ext ──────────────────
[[ -f "$SQL_CONNECT" ]] || die "Not found: $SQL_CONNECT"
dbuser=$(grep -E '^\s*user\s*=' "$SQL_CONNECT" | head -1 | sed 's/.*=\s*//' | tr -d '[:space:]')
dbpass=$(grep -E '^\s*password\s*=' "$SQL_CONNECT" | head -1 | sed 's/.*=\s*//' | tr -d '[:space:]')
dbname=$(grep -E '^\s*dbname\s*=' "$SQL_CONNECT" | head -1 | sed 's/.*=\s*//' | tr -d '[:space:]')
[[ -n "$dbuser" ]] || die "Could not parse 'user' from $SQL_CONNECT"
[[ -n "$dbpass" ]] || die "Could not parse 'password' from $SQL_CONNECT"
[[ -n "$dbname" ]] || die "Could not parse 'dbname' from $SQL_CONNECT"
info "DB : $dbuser@$dbname"
# ── backup ────────────────────────────────────────────────────────────────────
heading "Backup"
mkdir -p "$BACKUP_BASE"
BACKUP_DIR="${BACKUP_BASE}/quota_clone_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR"
step "$(basename "$DOVECOT_REAL") ..."
tar -czf "${BACKUP_DIR}/dovecot.tar.gz" \
-C "$(dirname "$DOVECOT_REAL")" "$(basename "$DOVECOT_REAL")" \
&& ok || { failed; die "Backup of $DOVECOT_REAL failed."; }
step "$(basename "$PFA_REAL") ..."
tar -czf "${BACKUP_DIR}/postfixadmin.tar.gz" \
-C "$(dirname "$PFA_REAL")" "$(basename "$PFA_REAL")" \
&& ok || { failed; die "Backup of $PFA_REAL failed."; }
info "Saved to: $BACKUP_DIR"
# ── PostgreSQL ────────────────────────────────────────────────────────────────
heading "PostgreSQL"
step "Drop old mergequota2 trigger + function ..."
psql -U"$dbuser" "$dbname" -c "
DROP TRIGGER IF EXISTS mergequota2 ON quota2;
DROP FUNCTION IF EXISTS merge_quota2();
" > /dev/null 2>&1 \
&& ok || { failed; die "Could not drop old mergequota2 trigger."; }
# Single-quoted heredoc: $$ is passed literally to psql (no shell expansion).
step "Create upsertquota2 trigger + function ..."
psql -U"$dbuser" "$dbname" <<'SQL' > /dev/null 2>&1
CREATE OR REPLACE FUNCTION upsert_quota2() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF NEW.bytes IS NULL OR NEW.bytes < 0 THEN NEW.bytes := 0; END IF;
IF NEW.messages IS NULL OR NEW.messages < 0 THEN NEW.messages := 0; END IF;
RETURN NEW;
END;
$$;
ALTER FUNCTION public.upsert_quota2() OWNER TO postfix;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'upsertquota2') THEN
CREATE TRIGGER upsertquota2
BEFORE INSERT ON quota2
FOR EACH ROW EXECUTE PROCEDURE upsert_quota2();
END IF;
END;
$$;
SQL
[[ $? -eq 0 ]] && ok || { failed; die "Could not create upsertquota2 trigger."; }
# ── Dovecot configuration ─────────────────────────────────────────────────────
heading "Dovecot configuration"
# 91-quota-clone.conf — always (re)written, content is idempotent
QUOTA_CLONE_CONF="${DOVECOT_CONF_D}/91-quota-clone.conf"
step "Write 91-quota-clone.conf ..."
cat > "$QUOTA_CLONE_CONF" <<EOF
##
## Quota Clone Configuration (Dovecot 2.4+)
##
## Writes quota usage to the quota2 table in real time so PostfixAdmin
## can display mailbox fill levels via \$CONF['used_quotas'] = 'YES'.
##
dict_server {
dict quota_clone_pgsql {
driver = sql
sql_driver = pgsql
pgsql localhost {
parameters {
user = $dbuser
password = $dbpass
dbname = $dbname
}
}
dict_map priv/quota/storage {
sql_table = quota2
username_field = username
value_field bytes {
}
}
dict_map priv/quota/messages {
sql_table = quota2
username_field = username
value_field messages {
}
}
}
}
EOF
[[ $? -eq 0 ]] && ok || { failed; die "Could not write $QUOTA_CLONE_CONF"; }
# 90-quota.conf — append only if not already present
QUOTA_CONF="${DOVECOT_CONF_D}/90-quota.conf"
step "Append quota_clone block to 90-quota.conf ..."
if grep -q "quota_clone_pgsql" "$QUOTA_CONF" 2>/dev/null; then
skip
else
cat >> "$QUOTA_CONF" <<'EOF'
##
## Quota Clone - write quota usage to quota2 table for PostfixAdmin
##
mail_plugins {
quota_clone = yes
}
quota_clone {
dict proxy {
name = quota_clone_pgsql
}
}
EOF
[[ $? -eq 0 ]] && ok || { failed; die "Could not append to $QUOTA_CONF"; }
fi
# ── PostfixAdmin ──────────────────────────────────────────────────────────────
heading "PostfixAdmin"
[[ -f "$PFA_CONFIG" ]] || die "Not found: $PFA_CONFIG"
# Sets $CONF['key'] = 'val' in config.local.php.
# - Already correct value → SKIP
# - Present with different value → update in place (perl, only non-commented lines)
# - Not present → append
pfa_set() {
local key="$1" val="$2"
step "\$CONF['${key}'] = '${val}' ..."
if grep -qF "\$CONF['${key}'] = '${val}';" "$PFA_CONFIG"; then
skip
return
fi
if grep -qF "\$CONF['${key}']" "$PFA_CONFIG"; then
# present with wrong value — update active (non-commented) line
perl -i -p -e "s|^(\s*\\\$CONF\['${key}'\]\s*=\s*)'[^']*'|\${1}'${val}'|" "$PFA_CONFIG" \
&& ok || { failed; die "Could not update \$CONF['${key}'] in $PFA_CONFIG"; }
else
# not present — append
{ echo ""; echo "\$CONF['${key}'] = '${val}';"; } >> "$PFA_CONFIG" \
&& ok || { failed; die "Could not append \$CONF['${key}'] to $PFA_CONFIG"; }
fi
}
pfa_set used_quotas YES
pfa_set new_quota_table YES
# ── /usr/local/bin/update_quota2.sh ──────────────────────────────────────────
heading "Batch update script"
# Single-quoted PYEOF: no shell expansion inside the Python source.
step "Install /usr/local/bin/update_quota2.sh ..."
cat > /usr/local/bin/update_quota2.sh <<'PYEOF'
#!/usr/bin/env python3
"""
Batch-update the quota2 table for all active mailboxes.
Uses 'doveadm quota get' (must run as root) to read current usage and
writes it to quota2 via INSERT ... ON CONFLICT DO UPDATE.
Usage:
update_quota2.sh [--dry-run] [--limit N]
Options:
--dry-run Print what would be done without touching the database.
--limit N Process only the first N mailboxes (useful for testing).
"""
import subprocess
import sys
DOVEADM = '/usr/local/dovecot/bin/doveadm'
PSQL = 'psql'
DB_USER = 'postfix'
DB_NAME = 'postfix'
def get_users(limit=None):
query = 'SELECT username FROM mailbox WHERE active = true ORDER BY username;'
if limit:
query = ('SELECT username FROM mailbox WHERE active = true '
'ORDER BY username LIMIT %d;' % limit)
result = subprocess.run(
[PSQL, '-U', DB_USER, DB_NAME, '-tAF', '|', '-c', query],
capture_output=True, text=True
)
return [u.strip() for u in result.stdout.strip().split('\n') if u.strip()]
def get_quota(user):
"""Returns (bytes, messages) or (None, None) if not available."""
result = subprocess.run(
[DOVEADM, 'quota', 'get', '-u', user],
capture_output=True, text=True
)
bytes_val = messages_val = None
for line in result.stdout.splitlines():
parts = line.split()
if len(parts) < 4:
continue
if parts[2] == 'STORAGE' and parts[1] != 'Type':
try:
bytes_val = int(parts[3]) * 1024 # KiB → bytes
except ValueError:
bytes_val = 0
elif parts[2] == 'MESSAGE' and parts[1] != 'Type':
try:
messages_val = int(parts[3])
except ValueError:
messages_val = 0
return bytes_val, messages_val
def upsert_quota2(user, bytes_val, messages_val):
sql = (
"INSERT INTO quota2 (username, bytes, messages) VALUES ('"
+ user.replace("'", "''")
+ "', " + str(bytes_val) + ", " + str(messages_val) + ") "
"ON CONFLICT (username) DO UPDATE "
"SET bytes = EXCLUDED.bytes, messages = EXCLUDED.messages;"
)
subprocess.run([PSQL, '-U', DB_USER, DB_NAME, '-c', sql],
capture_output=True)
def main():
dry_run = '--dry-run' in sys.argv
limit = None
if '--limit' in sys.argv:
idx = sys.argv.index('--limit')
try:
limit = int(sys.argv[idx + 1])
except (IndexError, ValueError):
print('Usage: update_quota2.sh [--dry-run] [--limit N]')
sys.exit(1)
users = get_users(limit)
total = len(users)
print('Processing %d mailboxes...' % total)
updated = skipped = 0
for i, user in enumerate(users, 1):
bytes_val, messages_val = get_quota(user)
if bytes_val is None or messages_val is None:
print(' [%d/%d] SKIP %s (no quota data)' % (i, total, user))
skipped += 1
continue
if dry_run:
print(' [%d/%d] DRY %s: %d bytes, %d messages'
% (i, total, user, bytes_val, messages_val))
else:
upsert_quota2(user, bytes_val, messages_val)
if i % 50 == 0 or i == total:
print(' [%d/%d] done' % (i, total))
updated += 1
print('Done: %d updated, %d skipped.' % (updated, skipped))
if __name__ == '__main__':
main()
PYEOF
chmod +x /usr/local/bin/update_quota2.sh
[[ $? -eq 0 ]] && ok || { failed; die "Could not install update_quota2.sh"; }
# ── Dovecot reload ────────────────────────────────────────────────────────────
heading "Dovecot reload"
step "Reloading Dovecot ..."
"${DOVECOT_REAL}/sbin/dovecot" reload \
&& ok || { failed; die "Dovecot reload failed."; }
# ── summary ───────────────────────────────────────────────────────────────────
echo
echo -e "${_bold}Done.${_nc}"
echo
info "Backup : $BACKUP_DIR"
info "Next : run the initial batch fill of quota2 for all mailboxes:"
info " /usr/local/bin/update_quota2.sh [--dry-run] [--limit 5]"
echo
+253
View File
@@ -9226,6 +9226,259 @@ EOF
fi # Renew file /usr/local/dovecot-${_version}/etc/dovecot/sql-dict.conf.ext
## -----------------------------------------------------------------
## --- quota_clone plugin (Dovecot 2.4+ / pgsql only)
## ---
## --- Writes quota usage to quota2 table in real time so PostfixAdmin
## --- can display mailbox fill levels ($CONF['used_quotas'] = 'YES').
## ---
## --- Replaces the old ADD-semantics mergequota2 trigger with a
## --- SET-semantics upsertquota2 trigger (validation only); the dict
## --- proxy service generates INSERT … ON CONFLICT DO UPDATE natively.
if [[ $dovecot_major_version -gt 2 ]] \
|| ( [[ $dovecot_major_version -eq 2 ]] && [[ $dovecot_minor_version -gt 3 ]] ); then
if [ "$db_driver" = "pgsql" ]; then
## - Drop old ADD-semantics trigger/function if present
echononl " Drop old mergequota2 trigger/function (if present).."
cat << EOF | psql -U$dbuser $dbname > /dev/null 2>&1
DROP TRIGGER IF EXISTS mergequota2 ON quota2;
DROP FUNCTION IF EXISTS merge_quota2();
EOF
if [ "$?" = 0 ]; then
echo -e "$rc_done"
else
echo -e "$rc_failed"
error "Dropping mergequota2 trigger/function failed"
fi
## - Create upsertquota2 trigger (validation / sanitise only)
echononl " Create upsertquota2 trigger/function.."
cat << 'EOF' | psql -U$dbuser $dbname > /dev/null 2>&1
CREATE OR REPLACE FUNCTION upsert_quota2() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.bytes IS NULL OR NEW.bytes < 0 THEN
NEW.bytes := 0;
END IF;
IF NEW.messages IS NULL OR NEW.messages < 0 THEN
NEW.messages := 0;
END IF;
RETURN NEW;
END;
$$;
ALTER FUNCTION public.upsert_quota2() OWNER TO postfix;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_trigger WHERE tgname = 'upsertquota2'
) THEN
CREATE TRIGGER upsertquota2
BEFORE INSERT ON quota2
FOR EACH ROW
EXECUTE PROCEDURE upsert_quota2();
END IF;
END;
$$;
EOF
if [ "$?" = 0 ]; then
echo -e "$rc_done"
else
echo -e "$rc_failed"
error "Creating upsertquota2 trigger/function failed"
fi
## - Create 91-quota-clone.conf
_conf_file="/usr/local/dovecot-${_version}/etc/dovecot/conf.d/91-quota-clone.conf"
echononl " Create '$(basename "${_conf_file}")' (dict_server for quota_clone).."
cat <<EOF > "${_conf_file}"
##
## Quota Clone Configuration (Dovecot 2.4+)
##
## Writes quota usage to the quota2 PostgreSQL table in real time so
## PostfixAdmin can display mailbox fill levels.
##
dict_server {
dict quota_clone_pgsql {
driver = sql
sql_driver = pgsql
pgsql localhost {
parameters {
user = $dbuser
password = $dbpassword
dbname = $dbname
}
}
dict_map priv/quota/storage {
sql_table = quota2
username_field = username
value_field bytes {
}
}
dict_map priv/quota/messages {
sql_table = quota2
username_field = username
value_field messages {
}
}
}
}
EOF
if [ "$?" = 0 ]; then
echo -e "$rc_done"
else
echo -e "$rc_failed"
error "Creating file '${_conf_file}' failed!"
fi
## - Append quota_clone plugin block to 90-quota.conf
_conf_file="/usr/local/dovecot-${_version}/etc/dovecot/conf.d/90-quota.conf"
echononl " Append quota_clone block to '$(basename "${_conf_file}")'.."
if ! grep -q "quota_clone_pgsql" "${_conf_file}" 2>/dev/null; then
cat <<'EOF' >> "${_conf_file}"
##
## Quota Clone - write quota usage to quota2 table for PostfixAdmin
##
mail_plugins {
quota_clone = yes
}
quota_clone {
dict proxy {
name = quota_clone_pgsql
}
}
EOF
if [ "$?" = 0 ]; then
echo -e "$rc_done"
else
echo -e "$rc_failed"
error "Appending quota_clone block to '${_conf_file}' failed!"
fi
else
echo -e "${rc_already_done}"
fi
## - Install /usr/local/bin/update_quota2.sh (initial batch fill)
echononl " Install /usr/local/bin/update_quota2.sh.."
cat <<'PYEOF' > /usr/local/bin/update_quota2.sh
#!/usr/bin/env python3
"""Batch-update quota2 table for all active mailboxes from doveadm quota get."""
import subprocess
import sys
DOVEADM = '/usr/local/dovecot/bin/doveadm'
PSQL = 'psql'
DB_USER = 'postfix'
DB_NAME = 'postfix'
def get_users(limit=None):
query = 'SELECT username FROM mailbox WHERE active = true ORDER BY username;'
if limit:
query = 'SELECT username FROM mailbox WHERE active = true ORDER BY username LIMIT %d;' % limit
result = subprocess.run(
[PSQL, '-U', DB_USER, DB_NAME, '-tAF', '|', '-c', query],
capture_output=True, text=True
)
return [u.strip() for u in result.stdout.strip().split('\n') if u.strip()]
def get_quota(user):
result = subprocess.run(
[DOVEADM, 'quota', 'get', '-u', user],
capture_output=True, text=True
)
bytes_val = None
messages_val = None
for line in result.stdout.splitlines():
parts = line.split()
if len(parts) < 4:
continue
if parts[2] == 'STORAGE' and parts[1] != 'Type':
try:
bytes_val = int(parts[3]) * 1024
except ValueError:
bytes_val = 0
elif parts[2] == 'MESSAGE' and parts[1] != 'Type':
try:
messages_val = int(parts[3])
except ValueError:
messages_val = 0
return bytes_val, messages_val
def upsert_quota2(user, bytes_val, messages_val):
cmd = [PSQL, '-U', DB_USER, DB_NAME, '-c',
"INSERT INTO quota2 (username, bytes, messages) VALUES ('"
+ user.replace("'", "''") + "', " + str(bytes_val) + ", "
+ str(messages_val)
+ ") ON CONFLICT (username) DO UPDATE SET bytes = EXCLUDED.bytes, messages = EXCLUDED.messages;"]
subprocess.run(cmd, capture_output=True)
def main():
dry_run = '--dry-run' in sys.argv
limit = None
if '--limit' in sys.argv:
idx = sys.argv.index('--limit')
try:
limit = int(sys.argv[idx + 1])
except (IndexError, ValueError):
print('Usage: update_quota2.sh [--dry-run] [--limit N]')
sys.exit(1)
users = get_users(limit)
total = len(users)
print('Processing %d mailboxes...' % total)
ok = 0
skip = 0
for i, user in enumerate(users, 1):
bytes_val, messages_val = get_quota(user)
if bytes_val is None or messages_val is None:
print(' [%d/%d] SKIP %s (no quota data)' % (i, total, user))
skip += 1
continue
if dry_run:
print(' [%d/%d] DRY %s: %d bytes, %d messages' % (i, total, user, bytes_val, messages_val))
else:
upsert_quota2(user, bytes_val, messages_val)
if i % 50 == 0 or i == total:
print(' [%d/%d] done' % (i, total))
ok += 1
print('Done: %d updated, %d skipped.' % (ok, skip))
if __name__ == '__main__':
main()
PYEOF
chmod +x /usr/local/bin/update_quota2.sh
if [ "$?" = 0 ]; then
echo -e "$rc_done"
else
echo -e "$rc_failed"
error "Installing /usr/local/bin/update_quota2.sh failed!"
fi
fi # db_driver = pgsql
fi # Dovecot 2.4+ quota_clone
# In order to support extra variable "quota_rule", also userdb's and
# passdb's SQL query have to update
#