added rate limiting

This commit is contained in:
Matthew Saunders Brown 2021-12-30 16:50:23 -08:00
parent dae4959817
commit 81a29508eb
8 changed files with 247 additions and 120 deletions

View File

@ -13,16 +13,17 @@ help()
echo "$thisfilename"
echo "Add domain to vmail system"
echo ""
echo "usage: $thisfilename -d <domain> [-l <limit>] [-q <quota>] [-s <status>]"
echo "usage: $thisfilename -d <domain> [-l <limit>] [-q <quota>] [-r <ratelimit>] [-s <status>]"
echo ""
echo " -h Print this help."
echo " -d Domain to add to vmail system."
echo " -l <limit> Maximum number of mailboxes for this domain."
echo " -q <quota> Default mailbox quota in GB."
echo " -s <status> 1 for enabled, 0 for disabled."
echo " -h Print this help."
echo " -d Domain to add to vmail system."
echo " -l <limit> Maximum number of mailboxes for this domain."
echo " -q <quota> Default mailbox quota in GB."
echo " -r <ratelimit> Default mailbox sending rate limit per hour, multiplied by 10 for limit per day."
echo " -s <status> 1 for enabled, 0 for disabled."
echo ""
echo " Defaults for all Options are configured in database."
echo " NULL for LIMIT or QUOTA means no limit."
echo " Defaults for all Options are configured in database."
echo " NULL for LIMIT or QUOTA means no limit."
exit
}
@ -80,6 +81,16 @@ if [ "$rowcount" -eq '0' ] ; then
exit 1
fi
fi
if [ ! -z "$ratelimit" ] ; then
if [[ "$ratelimit" == "NULL" ]]; then
dbquery="$dbquery, mbox_ratelimit_default=NULL"
elif [[ "$ratelimit" =~ ^[0-9]+$ ]]; then
dbquery="$dbquery, mbox_quota_default='$ratelimit'"
else
echo "ERROR: ratelimit (-r) must numeric or NULL"
exit 1
fi
fi
dbquery="$dbquery;"
# add domain to vmail database
eval $dbcmd $dbcmdopts "\"$dbquery\""

View File

@ -12,13 +12,14 @@ help()
thisfilename=$(basename -- "$0")
echo "Modify an existing domain."
echo ""
echo "usage: $thisfilename -d domain [OPTIONS]"
echo "usage: $thisfilename -d <domain> [OPTIONS]"
echo ""
echo " -h Print this help."
echo " -d Domain to be modified."
echo " -l LIMIT Mailbox limit - the maximum number of mailboxes under this domain."
echo " -q QUOTA Mailbox Quota Default in GB - default quota for new mailboxes under this domain."
echo " -s <0|1> Status. 1 for enabled, 0 for disabled. Default is in db structure and is normally set to 1."
echo " -h Print this help."
echo " -d Domain to be modified."
echo " -l <limit> Mailbox limit - the maximum number of mailboxes under this domain."
echo " -q <quota> Mailbox Quota Default in GB - default quota for new mailboxes under this domain."
echo " -r <ratelimit> Default mailbox sending rate limit per hour, multiplied by 10 for limit per day."
echo " -s <0|1> Status. 1 for enabled, 0 for disabled. Default is in db structure and is normally set to 1."
exit
}
@ -51,6 +52,18 @@ if [[ -n $quota ]]; then
fi
fi
# check for ratelimit update
if [[ -n $ratelimit ]]; then
# make ratelimit uppercase in case it is set to NULL
ratelimit=`echo $ratelimit | tr [:lower:] [:upper:]`
if [[ "$ratelimit" =~ ^[0-9]+$ ]] || [[ "$ratelimit" == "NULL" ]]; then
dbset=" mbox_ratelimit_default=$ratelimit"
else
echo "ERROR: ratelimit (-r) must numeric or NULL"
exit 1
fi
fi
# check for limit update
if [[ -n $limit ]]; then
# make limit uppercase in case it is set to NULL

View File

@ -13,14 +13,15 @@ help()
echo "$thisfilename"
echo "Add email account to vmail system"
echo ""
echo "usage: $thisfilename -e <email> -p <password> [-q <quota>] [-s <0|1>] [-j <0|1|2>] [-h]"
echo "usage: $thisfilename -e <email> -p <password> [-q <quota>] [-r <ratelimit>] [-s <0|1>] [-j <0|1|2>] [-h]"
echo ""
echo " -h Print this help."
echo " -e <email> Email address to add."
echo " -p <password> Unencrypted Password for new email address."
echo " -q <quota> Set mailbox quota in GB, otherwise default for this domain is used. NULL means no limit."
echo " -s <0|1> Status. 1 for enabled, 0 for disabled. Default is in db structure and is normally set to 1."
echo " -j <0|1|2> Filter Junk/Spam messages. 0 = no filtering. 1 = filter Junk only. 2 = filter Junk & Spam. Default is 2."
echo " -h Print this help."
echo " -e <email> Email address to add."
echo " -p <password> Unencrypted Password for new email address."
echo " -q <quota> Set mailbox quota in GB, otherwise default for this domain is used. NULL means no limit."
echo " -r <ratelimit> Hourly rate limit for sending, multiplied by 10 for the daily limit."
echo " -s <0|1> Status. 1 for enabled, 0 for disabled. Default is in db structure and is normally set to 1."
echo " -j <0|1|2> Filter Junk/Spam messages. 0 = no filtering. 1 = filter Junk only. 2 = filter Junk & Spam. Default is 2."
}
vmail:getoptions "$@"
@ -69,6 +70,18 @@ if [ "$rowcount" -eq '0' ] ; then
echo "ERROR: quota (-q) must numeric or NULL"
exit 1
fi
if [ -z "$ratelimit" ] ; then
# get mbox_ratelimit_default from domains table
ratelimit=`mysql --defaults-extra-file=$MYSQL_CONNECTION_INFO_FILE -s -r -N -e "SELECT mbox_ratelimit_default from vm_domains WHERE domain='$domain';"`
fi
if [[ "$ratelimit" == "NULL" ]]; then
dbcmd="$dbcmd, ratelimit=NULL"
elif [[ "$ratelimit" =~ ^[0-9]+$ ]]; then
dbcmd="$dbcmd, ratelimit=\"$ratelimit\""
else
echo "ERROR: ratelimit (-r) must numeric or NULL"
exit 1
fi
dbcmd="$dbcmd;'"
elif [ "$rowcount" -eq '1' ] ; then
echo "ERROR: $email already exists in vmail database."

View File

@ -48,7 +48,7 @@ dbquery="SELECT vm_mboxes.mbox, vm_domains.domain"
if [ -n "$verbose" ]; then
dbquery="$dbquery, vm_mboxes.passwd"
fi
dbquery="$dbquery, vm_mboxes.status, vm_mboxes.quota, vm_mboxes.filter FROM vm_domains, vm_mboxes WHERE vm_domains.id=vm_mboxes.domain_id"
dbquery="$dbquery, vm_mboxes.status, vm_mboxes.quota, vm_mboxes.ratelimit, vm_mboxes.filter FROM vm_domains, vm_mboxes WHERE vm_domains.id=vm_mboxes.domain_id"
if [ -n "$mbox" ] && [ -n "$domain" ]; then
# search for specific email address
dbquery="$dbquery AND vm_domains.domain='$domain' AND vm_mboxes.mbox='$mbox'"

View File

@ -15,12 +15,13 @@ help()
echo ""
echo "usage: $thisfilename -e <email> [OPTIONS]"
echo ""
echo " -h Print this help."
echo " -e <email> Email account to modify."
echo " -p <password> Set new password."
echo " -q <quota> Set mailbox quota in GB, otherwise default for this domain is used. NULL means no limit."
echo " -s <0|1> 1 for enabled, 0 for disabled. Default is in db structure and is normally set to 1."
echo " -j <0|1|2> Filter Junk/Spam message. 0 = no filtering. 1 = filter Junk only. 2 = filter Junk & Spam. Default is 2."
echo " -h Print this help."
echo " -e <email> Email account to modify."
echo " -p <password> Set new password."
echo " -q <quota> Set mailbox quota in GB, otherwise default for this domain is used. NULL means no limit."
echo " -r <ratelimit> Hourly rate limit for sending, multiplied by 10 for the daily limit."
echo " -s <0|1> 1 for enabled, 0 for disabled. Default is in db structure and is normally set to 1."
echo " -j <0|1|2> Filter Junk/Spam message. 0 = no filtering. 1 = filter Junk only. 2 = filter Junk & Spam. Default is 2."
exit
}
@ -55,6 +56,18 @@ if [ -n "$quota" ]; then
fi
fi
# check for ratelimit update
if [ -n "$ratelimit" ]; then
# make ratelimit uppercase in case it is set to NULL
ratelimit=`echo $ratelimit | tr [:lower:] [:upper:]`
if [[ "$ratelimit" =~ ^[0-9]+$ ]] || [[ "$ratelimit" == "NULL" ]]; then
dbset=" ratelimit=$ratelimit"
else
echo "ERROR: ratelimit (-r) must numeric or NULL"
exit 1
fi
fi
# check for password update
if [ ! -z "$password" ]; then
passwd=`doveadm -o stats_writer_socket_path= pw -s sha512-crypt -p "$password"`

View File

@ -85,7 +85,7 @@ function vmail::yesno() {
function vmail:getoptions () {
local OPTIND
while getopts "ha:b:d:e:f:gj:cp:q:s:tk:gl:m:vx" opt ; do
while getopts "ha:b:d:e:f:gj:cp:q:r:s:tk:gl:m:vx" opt ; do
case "${opt}" in
h ) # display help and exit
help
@ -181,6 +181,9 @@ function vmail:getoptions () {
q ) # quota
quota=${OPTARG}
;;
r ) # ratelimit - hourly limit for sending, multiplied by 10 for daily limit
ratelimit=${OPTARG}
;;
s ) # status - 0 or 1
status=${OPTARG}
;;

View File

@ -364,11 +364,39 @@ acl_rcpt_to:
accept
hosts = : +relay_from_hosts
# Accept if the message arrived over an authenticated connection,
# from any host. Again, these messages are usually from MUAs, so
# recipient verification is omitted.
#
# Get rate limit for user and log current rate.
# Hourly rate limit is extracted from db, multiplied by 10 to get daily rate limit.
# The idea being that the hourly rate limit should be a maximum, peak rate, not a sustained rate.
# The default ratelimit of 100000 is meant to be so high as to never be reached (no limit).
# The primary purpose of rate limiting is fighting spam, either by an abusive user or a
# compromised account, not to restrict legitimate users. Set your defaults & limits accordignly.
warn
authenticated = *
set acl_m_ratelimit_hourly = ${lookup mysql{SELECT vm_mboxes.ratelimit FROM vm_domains, vm_mboxes WHERE vm_domains.domain='${domain}' AND vm_mboxes.mbox='${local_part}' AND vm_domains.id = vm_mboxes.domain_id AND vm_mboxes.ratelimit IS NOT NULL}{$value}{"100000"}}
set acl_m_ratelimit_daily = ${eval: $acl_m_ratelimit_hourly * 10}
ratelimit = 0 / 1h / per_mail / strict / $authenticated_id
log_message = Sender rate $sender_rate/$sender_rate_period for $authenticated_id
# enforce hourly rate limit
deny
authenticated = *
ratelimit = $acl_m_ratelimit_hourly / 1h / per_mail / strict / $authenticated_id
message = Rate Limit of $sender_rate/$sender_rate_period exceeded. Try again later.
log_message = $authenticated_id exceeded rate limit of $sender_rate/$sender_rate_period
# enforce daily rate limit
deny
authenticated = *
ratelimit = $acl_m_ratelimit_daily / 1d / per_mail / strict / $authenticated_id
message = Rate Limit of $sender_rate/$sender_rate_period exceeded. Try again later.
log_message = $authenticated_id exceeded rate limit of $sender_rate/$sender_rate_period
# authenticated user did not exceed rate limits, accept message now
accept
authenticated = *

226
vmail.sql
View File

@ -4,17 +4,9 @@ SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Database: `vmail`
--
CREATE DATABASE IF NOT EXISTS `vmail` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE `vmail`;
-- --------------------------------------------------------
@ -22,114 +14,77 @@ USE `vmail`;
-- Table structure for table `vm_aliases`
--
CREATE TABLE IF NOT EXISTS `vm_aliases` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
CREATE TABLE `vm_aliases` (
`id` int(10) UNSIGNED NOT NULL,
`mbox_id` int(10) UNSIGNED NOT NULL,
`alias` varchar(128) NOT NULL,
PRIMARY KEY (`id`),
KEY `mbox_delete_aliases` (`mbox_id`)
`alias` varchar(128) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
--
-- RELATIONSHIPS FOR TABLE `vm_aliases`:
-- `mbox_id`
-- `vm_mboxes` -> `id`
--
-- --------------------------------------------------------
--
-- Table structure for table `vm_autoresponders`
--
CREATE TABLE IF NOT EXISTS `vm_autoresponders` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
CREATE TABLE `vm_autoresponders` (
`id` int(10) UNSIGNED NOT NULL,
`mbox_id` int(10) UNSIGNED NOT NULL,
`subject` varchar(128) NOT NULL,
`body` text NOT NULL,
`mode` enum('Vacation','Autoresponder') NOT NULL,
`status` tinyint(1) NOT NULL DEFAULT 1,
`start` datetime DEFAULT NULL,
`end` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `autoresponder` (`mbox_id`)
`end` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
--
-- RELATIONSHIPS FOR TABLE `vm_autoresponders`:
-- `mbox_id`
-- `vm_mboxes` -> `id`
--
-- --------------------------------------------------------
--
-- Table structure for table `vm_domains`
--
CREATE TABLE IF NOT EXISTS `vm_domains` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
CREATE TABLE `vm_domains` (
`id` int(11) UNSIGNED NOT NULL,
`domain` varchar(255) NOT NULL,
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '1 for enabled, 0 for disabled',
`mbox_limit` smallint(6) UNSIGNED DEFAULT NULL COMMENT 'Maximum number of mailboxes for this domain',
`mbox_quota_default` smallint(6) UNSIGNED DEFAULT NULL COMMENT 'Default mailbox quota in GB',
PRIMARY KEY (`id`),
KEY `domain` (`domain`(191))
`mbox_limit` smallint(6) UNSIGNED DEFAULT 10 COMMENT 'Maximum number of mailboxes for this domain',
`mbox_quota_default` smallint(6) UNSIGNED DEFAULT 1 COMMENT 'Default mailbox quota in GB',
`mbox_ratelimit_default` smallint(6) DEFAULT 120 COMMENT 'Default hourly rate limit for new mboxes'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
--
-- RELATIONSHIPS FOR TABLE `vm_domains`:
--
-- --------------------------------------------------------
--
-- Table structure for table `vm_filters`
--
CREATE TABLE IF NOT EXISTS `vm_filters` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
CREATE TABLE `vm_filters` (
`id` int(10) UNSIGNED NOT NULL,
`mbox_id` int(11) UNSIGNED NOT NULL,
`filter` text NOT NULL,
PRIMARY KEY (`id`),
KEY `mbox_delete_filters` (`mbox_id`)
`filter` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
--
-- RELATIONSHIPS FOR TABLE `vm_filters`:
-- `mbox_id`
-- `vm_mboxes` -> `id`
--
-- --------------------------------------------------------
--
-- Table structure for table `vm_forwards`
--
CREATE TABLE IF NOT EXISTS `vm_forwards` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
CREATE TABLE `vm_forwards` (
`id` int(10) UNSIGNED NOT NULL,
`mbox_id` int(10) UNSIGNED NOT NULL,
`forward_to` varchar(128) NOT NULL,
`save_local` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `mbox_id` (`mbox_id`)
`save_local` tinyint(1) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
--
-- RELATIONSHIPS FOR TABLE `vm_forwards`:
-- `mbox_id`
-- `vm_mboxes` -> `id`
--
-- --------------------------------------------------------
--
-- Table structure for table `vm_greylisting`
--
CREATE TABLE IF NOT EXISTS `vm_greylisting` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
CREATE TABLE `vm_greylisting` (
`id` bigint(20) UNSIGNED NOT NULL,
`relay_hostname` varchar(255) DEFAULT NULL,
`relay_ip` varchar(80) DEFAULT NULL,
`sender` varchar(255) DEFAULT NULL,
@ -140,56 +95,151 @@ CREATE TABLE IF NOT EXISTS `vm_greylisting` (
`create_time` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
`type` enum('AUTO','MANUAL') NOT NULL DEFAULT 'MANUAL',
`passcount` bigint(20) NOT NULL DEFAULT 0,
`blockcount` bigint(20) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
`blockcount` bigint(20) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
--
-- RELATIONSHIPS FOR TABLE `vm_greylisting`:
--
-- --------------------------------------------------------
--
-- Table structure for table `vm_greylisting_resenders`
--
CREATE TABLE IF NOT EXISTS `vm_greylisting_resenders` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
CREATE TABLE `vm_greylisting_resenders` (
`id` bigint(20) NOT NULL,
`hostname` varchar(255) NOT NULL,
`count` bigint(20) UNSIGNED NOT NULL DEFAULT 1,
`timestamp` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`)
`timestamp` timestamp NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
--
-- RELATIONSHIPS FOR TABLE `vm_greylisting_resenders`:
--
-- --------------------------------------------------------
--
-- Table structure for table `vm_mboxes`
--
CREATE TABLE IF NOT EXISTS `vm_mboxes` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
CREATE TABLE `vm_mboxes` (
`id` int(10) UNSIGNED NOT NULL,
`domain_id` int(10) UNSIGNED NOT NULL,
`mbox` varchar(128) NOT NULL,
`username` varchar(128) NOT NULL,
`passwd` char(128) NOT NULL,
`status` tinyint(1) NOT NULL DEFAULT 1,
`quota` int(10) UNSIGNED DEFAULT NULL,
`filter` tinyint(1) NOT NULL DEFAULT 2,
PRIMARY KEY (`id`),
KEY `email` (`domain_id`,`mbox`) USING BTREE
`ratelimit` smallint(6) DEFAULT NULL,
`filter` tinyint(4) NOT NULL DEFAULT 2
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
--
-- RELATIONSHIPS FOR TABLE `vm_mboxes`:
-- `domain_id`
-- `vm_domains` -> `id`
-- Indexes for dumped tables
--
--
-- Indexes for table `vm_aliases`
--
ALTER TABLE `vm_aliases`
ADD PRIMARY KEY (`id`),
ADD KEY `mbox_delete_aliases` (`mbox_id`);
--
-- Indexes for table `vm_autoresponders`
--
ALTER TABLE `vm_autoresponders`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `autoresponder` (`mbox_id`);
--
-- Indexes for table `vm_domains`
--
ALTER TABLE `vm_domains`
ADD PRIMARY KEY (`id`),
ADD KEY `domain` (`domain`(191));
--
-- Indexes for table `vm_filters`
--
ALTER TABLE `vm_filters`
ADD PRIMARY KEY (`id`),
ADD KEY `mbox_delete_filters` (`mbox_id`);
--
-- Indexes for table `vm_forwards`
--
ALTER TABLE `vm_forwards`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `mbox_id` (`mbox_id`);
--
-- Indexes for table `vm_greylisting`
--
ALTER TABLE `vm_greylisting`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `vm_greylisting_resenders`
--
ALTER TABLE `vm_greylisting_resenders`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `vm_mboxes`
--
ALTER TABLE `vm_mboxes`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `username` (`username`),
ADD KEY `email` (`domain_id`,`mbox`) USING BTREE;
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `vm_aliases`
--
ALTER TABLE `vm_aliases`
MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `vm_autoresponders`
--
ALTER TABLE `vm_autoresponders`
MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `vm_domains`
--
ALTER TABLE `vm_domains`
MODIFY `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `vm_filters`
--
ALTER TABLE `vm_filters`
MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `vm_forwards`
--
ALTER TABLE `vm_forwards`
MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `vm_greylisting`
--
ALTER TABLE `vm_greylisting`
MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `vm_greylisting_resenders`
--
ALTER TABLE `vm_greylisting_resenders`
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `vm_mboxes`
--
ALTER TABLE `vm_mboxes`
MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
--
-- Constraints for dumped tables
--
@ -224,7 +274,3 @@ ALTER TABLE `vm_forwards`
ALTER TABLE `vm_mboxes`
ADD CONSTRAINT `domain_delete_mboxes` FOREIGN KEY (`domain_id`) REFERENCES `vm_domains` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;