install_update_dovecot-2.4.sh: enhance quota_clone plugin support for MySQL and update documentation

This commit is contained in:
2026-07-02 02:11:55 +02:00
parent d917dc69bb
commit 17e858d51b
2 changed files with 570 additions and 141 deletions
+243 -4
View File
@@ -9227,14 +9227,15 @@ fi # Renew file /usr/local/dovecot-${_version}/etc/dovecot/sql-dict.conf.ext
## -----------------------------------------------------------------
## --- quota_clone plugin (Dovecot 2.4+ / pgsql only)
## --- quota_clone plugin (Dovecot 2.4+ / pgsql + mysql)
## ---
## --- 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.
## --- SET-semantics upsertquota2 trigger (validation only); Dovecot's
## --- dict service generates native UPSERT queries (pgsql: ON CONFLICT
## --- DO UPDATE; mysql: ON DUPLICATE KEY UPDATE).
if [[ $dovecot_major_version -gt 2 ]] \
|| ( [[ $dovecot_major_version -eq 2 ]] && [[ $dovecot_minor_version -gt 3 ]] ); then
@@ -9474,7 +9475,245 @@ PYEOF
error "Installing /usr/local/bin/update_quota2.sh failed!"
fi
fi # db_driver = pgsql
elif [ "$db_driver" = "mysql" ]; then
## - Create quota2 table if it does not yet exist
echononl " Create quota2 table if not exists.."
cat << EOF | mysql -u$dbuser -p$dbpassword $dbname > /dev/null 2>&1
CREATE TABLE IF NOT EXISTS quota2 (
username varchar(100) NOT NULL,
bytes bigint NOT NULL DEFAULT 0,
messages integer NOT NULL DEFAULT 0,
PRIMARY KEY (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
EOF
if [ "$?" = 0 ]; then
echo -e "$rc_done"
else
echo -e "$rc_failed"
error "Creating quota2 table failed"
fi
## - Drop old ADD-semantics trigger if present
echononl " Drop old mergequota2 trigger (if present).."
cat << EOF | mysql -u$dbuser -p$dbpassword $dbname > /dev/null 2>&1
DROP TRIGGER IF EXISTS mergequota2;
EOF
if [ "$?" = 0 ]; then
echo -e "$rc_done"
else
echo -e "$rc_failed"
error "Dropping mergequota2 trigger failed"
fi
## - Create upsertquota2 trigger (DELIMITER needed in MySQL batch mode)
echononl " Create upsertquota2 trigger.."
cat << 'EOF' | mysql -u$dbuser -p$dbpassword $dbname > /dev/null 2>&1
DROP TRIGGER IF EXISTS upsertquota2;
DELIMITER //
CREATE TRIGGER upsertquota2
BEFORE INSERT ON quota2
FOR EACH ROW
BEGIN
IF NEW.bytes IS NULL OR NEW.bytes < 0 THEN SET NEW.bytes = 0; END IF;
IF NEW.messages IS NULL OR NEW.messages < 0 THEN SET NEW.messages = 0; END IF;
END//
DELIMITER ;
EOF
if [ "$?" = 0 ]; then
echo -e "$rc_done"
else
echo -e "$rc_failed"
error "Creating upsertquota2 trigger 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+ / MariaDB / MySQL)
##
## Writes quota usage to the quota2 table in real time so
## PostfixAdmin can display mailbox fill levels.
##
dict_server {
dict quota_clone_mysql {
driver = sql
sql_driver = mysql
mysql $dbhost {
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_mysql" "${_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_mysql
}
}
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)
## - Variables $dbuser, $dbpassword, $dbhost, $dbname are expanded
## - into the Python script by the unquoted heredoc.
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'
MYSQL = 'mysql'
DB_USER = '$dbuser'
DB_PASS = '$dbpassword'
DB_SOCKET = '$dbhost'
DB_NAME = '$dbname'
def mysql_query(sql):
return subprocess.run(
[MYSQL, '-u', DB_USER, '-p' + DB_PASS, '--socket', DB_SOCKET,
DB_NAME, '-sNe', sql],
capture_output=True, text=True)
def get_users(limit=None):
query = 'SELECT username FROM mailbox WHERE active = 1 ORDER BY username;'
if limit:
query = 'SELECT username FROM mailbox WHERE active = 1 ORDER BY username LIMIT %d;' % limit
r = mysql_query(query)
return [u.strip() for u in r.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):
sql = ("REPLACE INTO quota2 (username, bytes, messages) VALUES ('"
+ user.replace("'", "\\'") + "', "
+ str(bytes_val) + ", " + str(messages_val) + ");")
mysql_query(sql)
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
fi # Dovecot 2.4+ quota_clone