├── .gitignore ├── logrotate.d └── ratelimit-policyd ├── install.sh ├── init.d └── ratelimit-policyd ├── mysql-schema.sql ├── README.md └── daemon.pl /.gitignore: -------------------------------------------------------------------------------- 1 | # Project files 2 | ###################### 3 | .idea 4 | 5 | # OS generated files 6 | ###################### 7 | .DS_Store 8 | .DS_Store? 9 | ._* 10 | .Spotlight-V100 11 | .Trashes 12 | ehthumbs.db 13 | Thumbs.db 14 | -------------------------------------------------------------------------------- /logrotate.d/ratelimit-policyd: -------------------------------------------------------------------------------- 1 | /var/log/ratelimit-policyd.log { 2 | weekly 3 | missingok 4 | rotate 26 5 | dateext 6 | compress 7 | # delaycompress 8 | notifempty 9 | create 644 postfix postfix 10 | sharedscripts 11 | postrotate 12 | invoke-rc.d ratelimit-policyd restart 2>/dev/null >/dev/null || true 13 | endscript 14 | } 15 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # get current directory 4 | DIR="$( cd "$( dirname "$0" )" && pwd )" 5 | cd "$DIR" 6 | 7 | # assure our logfile belongs to user postfix 8 | touch /var/log/ratelimit-policyd.log 9 | chown postfix:postfix /var/log/ratelimit-policyd.log 10 | 11 | # install init script 12 | chmod 755 daemon.pl init.d/ratelimit-policyd 13 | ln -sf "$DIR/init.d/ratelimit-policyd" /etc/init.d/ 14 | insserv ratelimit-policyd 15 | 16 | # install logrotation configuration 17 | ln -sf "$DIR/logrotate.d/ratelimit-policyd" /etc/logrotate.d/ 18 | -------------------------------------------------------------------------------- /init.d/ratelimit-policyd: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: ratelimit-policyd 5 | # Required-Start: $all 6 | # Required-Stop: $local_fs $remote_fs $syslog $named $network 7 | # Should-Start: 8 | # Should-Stop: 9 | # Default-Start: 2 3 4 5 10 | # Default-Stop: 0 1 6 11 | # Short-Description: start SMTP Policy Daemon 12 | # Description: SMTP Rate Limit Policy Daemon 13 | ### END INIT INFO 14 | 15 | export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 16 | 17 | INSTALL_DIR=/opt/ratelimit-policyd 18 | DAEMON=$INSTALL_DIR/daemon.pl 19 | STOPSCRIPT=/usr/bin/perl 20 | RUNAS=postfix 21 | NAME=ratelimit-policyd 22 | PIDFILE=/var/run/$NAME.pid 23 | DESC="SMTP Rate Limit Policy Daemon" 24 | 25 | test -x $DAEMON || exit 0 26 | 27 | set -e 28 | 29 | case "$1" in 30 | start) 31 | echo -n "Starting $DESC: $NAME" 32 | start-stop-daemon --start --background --make-pidfile --pidfile $PIDFILE --exec $DAEMON --chuid $RUNAS 33 | echo "." 34 | ;; 35 | status) 36 | $DAEMON printshm 37 | ;; 38 | stop) 39 | echo -n "Stopping $DESC: $NAME" 40 | start-stop-daemon --stop --oknodo --pidfile $PIDFILE --exec $STOPSCRIPT && rm -f $PIDFILE 41 | echo "." 42 | ;; 43 | restart|force-reload) 44 | # 45 | # If the "reload" option is implemented, move the "force-reload" 46 | # option to the "reload" entry above. If not, "force-reload" is 47 | # just the same as "restart". 48 | # 49 | $0 stop && $0 start 50 | ;; 51 | *) 52 | N=/etc/init.d/$NAME 53 | echo "Usage: $N {start|stop|restart|force-reload|status}" >&2 54 | exit 1 55 | ;; 56 | esac 57 | 58 | exit 0 59 | -------------------------------------------------------------------------------- /mysql-schema.sql: -------------------------------------------------------------------------------- 1 | -- User: policyd@localhost 2 | -- GRANT USAGE ON *.* TO policyd@'localhost' IDENTIFIED BY '********'; 3 | -- GRANT SELECT, INSERT, UPDATE, DELETE ON policyd.* TO policyd@'localhost'; 4 | 5 | -- ----------------------------------------------------- 6 | -- Schema policyd 7 | -- ----------------------------------------------------- 8 | CREATE SCHEMA IF NOT EXISTS `policyd` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; 9 | 10 | USE `policyd`; 11 | 12 | -- ----------------------------------------------------- 13 | -- Table `policyd`.`ratelimit` 14 | -- ----------------------------------------------------- 15 | CREATE TABLE IF NOT EXISTS `ratelimit` ( 16 | `id` INT(11) NOT NULL AUTO_INCREMENT, 17 | `sender` VARCHAR(255) CHARACTER SET 'utf8' COLLATE 'utf8_bin' NOT NULL COMMENT 'sender address (SASL username)', 18 | `persist` TINYINT(1) NOT NULL DEFAULT '0' COMMENT 'Do not reset the given quota to the default value after expiry reached.', 19 | `quota` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT 'hourly|daily|weekly|monthly recipient quota limit', 20 | `used` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT 'current recipient counter', 21 | `updated` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'when used counter was last updated', 22 | `expiry` INT(10) UNSIGNED DEFAULT '0' COMMENT 'expiry (Unix-timestamp) after which the counter gets reset', 23 | PRIMARY KEY (`id`), 24 | UNIQUE INDEX `idx_sender` (`sender` ASC)) 25 | ENGINE = InnoDB 26 | DEFAULT CHARACTER SET = utf8 27 | COLLATE = utf8_general_ci; 28 | 29 | -- ----------------------------------------------------- 30 | -- Table `policyd`.`view_ratelimit` 31 | -- ----------------------------------------------------- 32 | CREATE OR REPLACE VIEW `view_ratelimit` AS SELECT *, FROM_UNIXTIME(`expiry`) AS `expirytime` FROM `ratelimit`; 33 | 34 | 35 | -- ----------------------------------------------------- 36 | -- PATCHES 37 | -- ----------------------------------------------------- 38 | 39 | -- patch 001 (2015-01-10) 40 | /* 41 | ALTER TABLE `ratelimit` ADD `updated` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `used`; 42 | UPDATE `ratelimit` SET `updated` = NOW(); 43 | */ 44 | 45 | -- patch 002 (2015-01-10) 46 | /* 47 | ALTER TABLE `ratelimit` MODIFY `sender` VARCHAR(255) CHARACTER SET 'utf8' COLLATE 'utf8_bin' NOT NULL COMMENT 'sender address (SASL username)'; 48 | ALTER TABLE `ratelimit` MODIFY `quota` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT 'hourly|daily|weekly|monthly recipient quota limit'; 49 | ALTER TABLE `ratelimit` MODIFY `used` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT 'current recipient counter'; 50 | ALTER TABLE `ratelimit` MODIFY `updated` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'when used counter was last updated'; 51 | ALTER TABLE `ratelimit` MODIFY `expiry` INT(10) UNSIGNED DEFAULT '0' COMMENT 'expiry (Unix-timestamp) after which the counter gets reset'; 52 | ALTER TABLE `ratelimit` ADD `persist` TINYINT(1) NOT NULL DEFAULT '0' COMMENT 'Do not reset the given quota to the default value after expiry reached.' AFTER `sender`; 53 | */ 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ratelimit-policyd 2 | 3 | A Sender rate limit policy daemon for Postfix. 4 | 5 | Original Copyright (c) Onlime Webhosting (http://www.onlime.ch) 6 | 7 | Modified by Mathieu Pellegrin for WellHosted (http://www.wellhosted.ch) 8 | 9 | ## Credits 10 | 11 | This project was forked from onlime/ratelimit-policyd and modified to ensure that only authenticated users are counted for quota. All credits go to Simone Caruso for his original work (bejelith/send_rate_policyd). 12 | 13 | ## Purpose 14 | 15 | This small Perl daemon **limits the number of emails** sent by users through your Postfix server, and store message quota in a RDMS system (MySQL). It counts the number of recipients for each sent email. You can setup a send rate per user or sender domain (via SASL username) on **daily/weekly/monthly** basis. 16 | 17 | **The program uses the Postfix policy delegation protocol to control access to the mail system before a message has been accepted (please visit [SMTPD_POLICY_README.html](http://www.postfix.org/SMTPD_POLICY_README.html) for more information).** 18 | 19 | For a long time we were using Postfix-Policyd v1 (the old 1.82) in production instead, but that project was no longer maintained and the successor [PolicyD v2 (codename "cluebringer")](http://wiki.policyd.org/) got overly complex and badly documented. Also, PolicyD seems to have been abandoned since 2013. 20 | 21 | ratelimit-policyd will never be as feature-rich as other policy daemons. Its main purpose is to limit the number of emails per account, nothing more and nothing less. We focus on performance and simplicity. 22 | 23 | **This daemon caches the quota in memory, so you don't need to worry about I/O operations!** 24 | 25 | ## New Features 26 | 27 | The original forked code from [bejelith/send_rate_policyd](https://github.com/bejelith/send_rate_policyd) was improved with the following new features: 28 | 29 | - automatically inserts new SASL-users (upon first email sent) 30 | - Debian default init.d startscript 31 | - added installer and documentation 32 | - bugfix: weekly mode did not work (expiry date was not correctly calculated) 33 | - bugfix: counters did not get reset after expiry 34 | - additional information in DB: updated timestamp 35 | - added view_ratelimit in DB to make Unix timestamps human readable (default datetime format) 36 | - syslog messaging (similar to Postfix-policyd) including all relevant information and counter/quota 37 | - more detailed logging 38 | - added logrotation script for /var/log/ratelimit-policyd.log 39 | - added flag in ratelimit DB table to make specific quotas persistent (all others will get reset to default after expiry) 40 | - continue raising counter even in over quota state 41 | 42 | The script from Onlime Webhosting was modified to : 43 | - Support smtpd_sender_restrictions (triggerd only on successful SASL login) instead of smtpd_data_restrictions (triggered when processing any outgoing mail) 44 | - As a consequence, the script is neutral for ISPConfig auto reply, auto forward, and any mail sent by Postfix without authentication (it will not count +1 on the quota for system mails, as long as your $mynetworks is configured accordingly) 45 | 46 | ## Installation 47 | 48 | Recommended installation: 49 | 50 | ```bash 51 | $ cd /opt/ 52 | $ git clone https://github.com/onlime/ratelimit-policyd.git ratelimit-policyd 53 | $ cd ratelimit-policyd 54 | $ chmod +x install.sh 55 | $ ./install.sh 56 | ``` 57 | 58 | Create the DB schema and user: 59 | 60 | ```bash 61 | $ mysql -u root -p < mysql-schema.sql 62 | ``` 63 | 64 | ```sql 65 | GRANT USAGE ON *.* TO policyd@'localhost' IDENTIFIED BY '********'; 66 | GRANT SELECT, INSERT, UPDATE, DELETE ON policyd.* TO policyd@'localhost'; 67 | ``` 68 | 69 | Adjust configuration options in ```daemon.pl```: 70 | 71 | ```perl 72 | ### CONFIGURATION SECTION 73 | my @allowedhosts = ('127.0.0.1', '10.0.0.1'); 74 | my $LOGFILE = "/var/log/ratelimit-policyd.log"; 75 | my $PIDFILE = "/var/run/ratelimit-policyd.pid"; 76 | my $SYSLOG_IDENT = "ratelimit-policyd"; 77 | my $SYSLOG_LOGOPT = "ndelay,pid"; 78 | my $SYSLOG_FACILITY = LOG_MAIL; 79 | chomp( my $vhost_dir = `pwd`); 80 | my $port = 10032; 81 | my $listen_address = '127.0.0.1'; # or '0.0.0.0' 82 | my $s_key_type = 'email'; # domain or email 83 | my $dsn = "DBI:mysql:policyd:127.0.0.1"; 84 | my $db_user = 'policyd'; 85 | my $db_passwd = '************'; 86 | my $db_table = 'ratelimit'; 87 | my $db_quotacol = 'quota'; 88 | my $db_tallycol = 'used'; 89 | my $db_updatedcol = 'updated'; 90 | my $db_expirycol = 'expiry'; 91 | my $db_wherecol = 'sender'; 92 | my $deltaconf = 'daily'; # hourly|daily|weekly|monthly 93 | my $defaultquota = 1000; 94 | my $sql_getquota = "SELECT $db_quotacol, $db_tallycol, $db_expirycol FROM $db_table WHERE $db_wherecol = ? AND $db_quotacol > 0"; 95 | my $sql_updatequota = "UPDATE $db_table SET $db_tallycol = $db_tallycol + ?, $db_updatedcol = NOW(), $db_expirycol = ? WHERE $db_wherecol = ?"; 96 | my $sql_updatereset = "UPDATE $db_table SET $db_tallycol = ?, $db_updatedcol = NOW(), $db_expirycol = ? WHERE $db_wherecol = ?"; 97 | my $sql_insertquota = "INSERT INTO $db_table ($db_wherecol, $db_quotacol, $db_tallycol, $db_expirycol) VALUES (?, ?, ?, ?)"; 98 | ### END OF CONFIGURATION SECTION 99 | ``` 100 | 101 | **Take care of using a port higher than 1024 to run the script as non-root (our init script runs it as user "postfix").** 102 | 103 | In most cases, the default configuration should be fine. Just don't forget to paste your DB password in ``$db_password``. 104 | 105 | Now, start the daemon: 106 | 107 | ```bash 108 | $ service ratelimit-policyd start 109 | ``` 110 | 111 | ## Testing 112 | 113 | Check if the daemon is really running: 114 | 115 | ```bash 116 | $ netstat -tl | grep 10032 117 | tcp 0 0 localhost.localdo:10032 *:* LISTEN 118 | 119 | $ cat /var/run/ratelimit-policyd.pid 120 | 30566 121 | 122 | $ ps aux | grep daemon.pl 123 | postfix 30566 0.4 0.1 176264 19304 ? Ssl 14:37 0:00 /opt/send_rate_policyd/daemon.pl 124 | 125 | $ pstree -p | grep ratelimit 126 | init(1)-+-/opt/ratelimit-(11298)-+-{/opt/ratelimit-}(11300) 127 | | |-{/opt/ratelimit-}(11301) 128 | | |-{/opt/ratelimit-}(11302) 129 | | |-{/opt/ratelimit-}(14834) 130 | | |-{/opt/ratelimit-}(15001) 131 | | |-{/opt/ratelimit-}(15027) 132 | | |-{/opt/ratelimit-}(15058) 133 | | `-{/opt/ratelimit-}(15065) 134 | 135 | ``` 136 | 137 | Print the cache content (in shared memory) with update statistics: 138 | 139 | ```bash 140 | $ service ratelimit-policyd status 141 | Printing shm: 142 | Domain : Quota : Used : Expire 143 | Threads running: 6, Threads waiting: 2 144 | ``` 145 | 146 | ## Postfix Configuration 147 | 148 | Modify the postfix data restriction class ```smtpd_sender_restrictions``` like the following, ```/etc/postfix/main.cf```: 149 | 150 | ``` 151 | smtpd_sender_restrictions = check_policy_service inet:$IP:$PORT 152 | ``` 153 | 154 | sample configuration (chained with classic SASL authentication from MySQL): 155 | 156 | ``` 157 | smtpd_sender_restrictions = check_sender_access mysql:/etc/postfix/clients.cf, check_policy_service inet:127.0.0.1:10032 158 | ``` 159 | 160 | If you're sure that ratelimit-policyd is really running, restart Postfix: 161 | 162 | ``` 163 | $ service postfix restart 164 | ``` 165 | 166 | ## Logging 167 | 168 | Detailed logging is written to ``/var/log/ratelimit-policyd.log```. In addition, the most important information including the counter status is written to syslog: 169 | 170 | ``` 171 | $ tail -f /var/log/ratelimit-policyd.log 172 | Sat Jan 10 12:08:37 2015 Looking for demo@example.com 173 | Sat Jan 10 12:08:37 2015 07F452AC009F: client=4-3.2-1.cust.example.com[1.2.3.4], sasl_method=PLAIN, sasl_username=demo@example.com, recipient_count=1, curr_count=6/1000, status=UPDATE 174 | 175 | $ grep ratelimit-policyd /var/log/syslog 176 | Jan 10 12:08:37 mx1 ratelimit-policyd[2552]: 07F452AC009F: client=4-3.2-1.cust.example.com[1.2.3.4], sasl_method=PLAIN, sasl_username=demo@example.com, recipient_count=1, curr_count=6/1000, status=UPDATE 177 | ``` 178 | -------------------------------------------------------------------------------- /daemon.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use Socket; 3 | use POSIX ; 4 | use DBI; 5 | use Sys::Syslog; 6 | use Switch; 7 | use threads; 8 | use threads::shared; 9 | use Thread::Semaphore; 10 | use File::Basename; 11 | my $semaphore = new Thread::Semaphore; 12 | 13 | ### CONFIGURATION SECTION 14 | my @allowedhosts = ('127.0.0.1'); 15 | my $LOGFILE = "/var/log/ratelimit-policyd.log"; 16 | my $PIDFILE = "/var/run/ratelimit-policyd.pid"; 17 | my $SYSLOG_IDENT = "ratelimit-policyd"; 18 | my $SYSLOG_LOGOPT = "ndelay,pid"; 19 | my $SYSLOG_FACILITY = LOG_MAIL; 20 | chomp( my $vhost_dir = `pwd`); 21 | my $port = 10032; 22 | my $listen_address = '127.0.0.1'; # or '0.0.0.0' 23 | my $s_key_type = 'email'; # domain or email 24 | my $dsn = "DBI:mysql:policyd:127.0.0.1"; 25 | my $db_user = 'policyd'; 26 | my $db_passwd = '************'; 27 | my $db_table = 'ratelimit'; 28 | my $db_quotacol = 'quota'; 29 | my $db_tallycol = 'used'; 30 | my $db_updatedcol = 'updated'; 31 | my $db_expirycol = 'expiry'; 32 | my $db_wherecol = 'sender'; 33 | my $db_persistcol = 'persist'; 34 | my $deltaconf = 'daily'; # hourly|daily|weekly|monthly 35 | my $defaultquota = 300; # 300 recipients by day and by user, CC and CCI count as well 36 | my $sql_getquota = "SELECT $db_quotacol, $db_tallycol, $db_expirycol, $db_persistcol FROM $db_table WHERE $db_wherecol = ? AND $db_quotacol > 0"; 37 | my $sql_updatequota = "UPDATE $db_table SET $db_tallycol = $db_tallycol + ?, $db_updatedcol = NOW(), $db_expirycol = ? WHERE $db_wherecol = ?"; 38 | my $sql_updatereset = "UPDATE $db_table SET $db_quotacol = ?, $db_tallycol = ?, $db_updatedcol = NOW(), $db_expirycol = ? WHERE $db_wherecol = ?"; 39 | my $sql_insertquota = "INSERT INTO $db_table ($db_wherecol, $db_quotacol, $db_tallycol, $db_expirycol) VALUES (?, ?, ?, ?)"; 40 | ### END OF CONFIGURATION SECTION 41 | 42 | $0=join(' ',($0,@ARGV)); 43 | 44 | if ($ARGV[0] eq "printshm") { 45 | my $out = `echo "printshm"|nc $listen_address $port`; 46 | print $out; 47 | exit(0); 48 | } 49 | my %quotahash :shared; 50 | my %scoreboard :shared; 51 | my $lock:shared; 52 | my $cnt=0; 53 | my $proto = getprotobyname('tcp'); 54 | my $thread_count = 3; 55 | my $min_threads = 2; 56 | # create a socket, make it reusable 57 | socket(SERVER, PF_INET, SOCK_STREAM, $proto) or die "socket: $!"; 58 | setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR, 1) or die "setsock: $!"; 59 | my $paddr = sockaddr_in($port, inet_aton($listen_address)); #Server sockaddr_in 60 | bind(SERVER, $paddr) or die "bind: $!";# bind to a port, then listen 61 | listen(SERVER, SOMAXCONN) or die "listen: $!"; 62 | 63 | # initialize syslog 64 | openlog($SYSLOG_IDENT, $SYSLOG_LOGOPT, $SYSLOG_FACILITY); 65 | 66 | #&daemonize; 67 | &prepare_log; 68 | 69 | $SIG{TERM} = \&sigterm_handler; 70 | $SIG{HUP} = \&print_cache; 71 | while (1) { 72 | my $i = 0; 73 | my @threads; 74 | while($i < $thread_count) { 75 | #$threads[$i] = threads->new(\&start_thr)->detach(); 76 | threads->new(\&start_thr); 77 | logger("Started thead num $i."); 78 | $i++; 79 | } 80 | while(1) { 81 | sleep 5; 82 | $cnt++; 83 | my $r = 0; 84 | my $w = 0; 85 | if ($cnt % 6 == 0) { 86 | lock($lock); 87 | &commit_cache; 88 | &flush_cache; 89 | logger("Master: cache committed and flushed"); 90 | } 91 | while (my ($k, $v) = each(%scoreboard)) { 92 | if ($v eq 'running') { 93 | $r++; 94 | } else { 95 | $w++; 96 | } 97 | } 98 | if ($r/($r + $w) > 0.9) { 99 | threads->new(\&start_thr); 100 | logger("New thread started"); 101 | } 102 | if ($cnt % 150 == 0) { 103 | logger("STATS: threads running: $r, threads waiting $w."); 104 | } 105 | } 106 | } 107 | 108 | exit; 109 | 110 | sub start_thr { 111 | my $threadid = threads->tid(); 112 | my $client_addr; 113 | my $client_ipnum; 114 | my $client_ip; 115 | my $client; 116 | while(1) { 117 | $scoreboard{$threadid} = 'waiting'; 118 | $semaphore->down();#TODO move to non-block 119 | $client_addr = accept($client, SERVER); 120 | $semaphore->up(); 121 | $scoreboard{$threadid} = 'running'; 122 | if (!$client_addr) { 123 | logger("TID: $threadid accept() failed with: $!"); 124 | next; 125 | } 126 | my ($client_port, $client_ip) = unpack_sockaddr_in($client_addr); 127 | $client_ipnum = inet_ntoa($client_ip); 128 | logger("TID: $threadid accepted from $client_ipnum ..."); 129 | 130 | select($client); 131 | $|=1; 132 | 133 | if (grep $_ eq $client_ipnum, @allowedhosts) { 134 | #my $client_host = gethostbyaddr($client_ip, AF_INET); 135 | #if (! defined ($client_host)) { $client_host=$client_ipnum;} 136 | my $message; 137 | my @buf; 138 | while(!eof($client)) { 139 | $message = <$client>; 140 | if ($message =~ m/printshm/) { 141 | my $r=0; 142 | my $w =0; 143 | print $client "Printing shm:\r\n"; 144 | print $client "Domain\t\t:\tQuota\t:\tUsed\t:\tExpire\r\n"; 145 | while(($k,$v) = each(%quotahash)) { 146 | chomp(my $exp = ctime($quotahash{$k}{'expire'})); 147 | print $client "$k\t:\t".$quotahash{$k}{'quota'}."\t:\t $quotahash{$k}{'tally'}\t:\t$exp\r\n"; 148 | } 149 | while (my ($k, $v) = each(%scoreboard)) { 150 | if ($v eq 'running') { 151 | $r++; 152 | } else { 153 | $w++; 154 | } 155 | } 156 | print $client "Threads running: $r, Threads waiting: $w\r\n"; 157 | last; 158 | } elsif ($message =~ m/=/) { 159 | push(@buf, $message); 160 | next; 161 | } elsif ($message == "\r\n") { 162 | #logger("Handle new request"); 163 | my $ret = &handle_req(@buf); 164 | if ($ret =~ m/unknown/) { 165 | last; 166 | #New thread model - old code 167 | # shutdown($client,2); 168 | #?? threads->exit(0); 169 | } else { 170 | print $client "action=$ret\n\n"; 171 | } 172 | @buf = (); 173 | } else { 174 | print $client "message not understood\r\n"; 175 | } 176 | } 177 | } else { 178 | logger("Client $client_ipnum connection not allowed."); 179 | } 180 | shutdown($client,2); 181 | undef $client; 182 | logger("TID: $threadid Client $client_ipnum disconnected."); 183 | } 184 | undef $scoreboard{$threadid}; 185 | threads->exit(0); 186 | } 187 | 188 | sub handle_req { 189 | my @buf = @_; 190 | my $protocol_state; 191 | my $sasl_method; 192 | my $sasl_username; 193 | my $recipient_count; 194 | my $queue_id; 195 | my $client_address; 196 | my $client_name; 197 | local $/ = "\n"; 198 | foreach $aline(@buf) { 199 | my @line = split("=", $aline); 200 | chomp(@line); 201 | #logger("DEBUG ". $line[0] ."=". $line[1]); 202 | switch($line[0]) { 203 | case "protocol_state" { 204 | chomp($protocol_state = $line[1]); 205 | } 206 | case "sasl_method"{ 207 | chomp($sasl_method = $line[1]); 208 | } 209 | case "sasl_username"{ 210 | chomp($sasl_username = $line[1]); 211 | } 212 | case "recipient_count"{ 213 | chomp($recipient_count = $line[1]); 214 | } 215 | case "queue_id"{ 216 | chomp($queue_id = $line[1]); 217 | } 218 | case "client_address"{ 219 | chomp($client_address = $line[1]); 220 | } 221 | case "client_name"{ 222 | chomp($client_name = $line[1]); 223 | } 224 | } 225 | } 226 | 227 | if ($protocol_state !~ m/RCPT/ || $sasl_username eq "" ) { 228 | # It should not happen if check_policy is chained after proper authentication on smtpd_sender_restrictions 229 | logger("protocol_state=$protocol_state sasl_username=$sasl_username"); 230 | return "dunno"; 231 | } else { 232 | # One RCPT is triggered by outgoing address, including CC and CCI, no need to count 233 | $recipient_count = 1; 234 | } 235 | 236 | my $skey = ''; 237 | if ($s_key_type eq 'domain') { 238 | $skey = (split("@", $sasl_username))[1]; 239 | } else { 240 | $skey = $sasl_username; 241 | } 242 | 243 | my $syslogMsg; 244 | my $syslogMsgTpl = sprintf("%s: client=%s[%s], sasl_method=%s, sasl_username=%s, recipient_count=%s, curr_count=%%s/%%s, status=%%s", 245 | $queue_id, $client_name, $client_address, $sasl_method, $sasl_username, $recipient_count); 246 | 247 | #TODO: Maybe i should move to semaphore!!! 248 | lock($lock); 249 | if (!exists($quotahash{$skey})) { 250 | logger("Looking for $skey"); 251 | my $dbh = get_db_handler() 252 | or return "dunno";; 253 | my $sql_query = $dbh->prepare($sql_getquota); 254 | $sql_query->execute($skey); 255 | if ($sql_query->rows > 0) { 256 | while(@row = $sql_query->fetchrow_array()) { 257 | $quotahash{$skey} = &share({}); 258 | $quotahash{$skey}{'quota'} = $row[0]; 259 | $quotahash{$skey}{'tally'} = $row[1]; 260 | $quotahash{$skey}{'sum'} = 0; 261 | $quotahash{$skey}{'expire'} = $row[2]; 262 | $quotahash{$skey}{'persist'} = $row[3]; 263 | undef @row; 264 | } 265 | $sql_query->finish(); 266 | $dbh->disconnect; 267 | } else { 268 | $sql_query->finish(); 269 | my $expire = calcexpire($deltaconf); 270 | $sql_query = $dbh->prepare($sql_insertquota); 271 | logger("Inserting $skey, $defaultquota, $recipient_count, $expire"); 272 | $sql_query->execute($skey, $defaultquota, $recipient_count, $expire) 273 | or logger("Query error: ". $sql_query->errstr); 274 | $sql_query->finish(); 275 | $dbh->disconnect; 276 | $syslogMsg = sprintf($syslogMsgTpl, $recipient_count, $defaultquota, "INSERT"); 277 | logger($syslogMsg); 278 | syslog(LOG_NOTICE, $syslogMsg); 279 | return "dunno"; 280 | } 281 | } 282 | if ($quotahash{$skey}{'expire'} < time()) { 283 | lock($lock); 284 | $quotahash{$skey}{'sum'} = 0; 285 | $quotahash{$skey}{'tally'} = 0; 286 | $quotahash{$skey}{'expire'} = calcexpire($deltaconf); 287 | my $newQuota = ($quotahash{$skey}{'persist'}) ? $quotahash{$skey}{'quota'} : $defaultquota; 288 | my $dbh = get_db_handler() 289 | or return "dunno";; 290 | my $sql_query = $dbh->prepare($sql_updatereset); 291 | $sql_query->execute($newQuota, 0, $quotahash{$skey}{'expire'}, $skey) 292 | or logger("Query error: ". $sql_query->errstr); 293 | } 294 | $quotahash{$skey}{'tally'} += $recipient_count; 295 | $quotahash{$skey}{'sum'} += $recipient_count; 296 | if ($quotahash{$skey}{'tally'} > $quotahash{$skey}{'quota'}) { 297 | $syslogMsg = sprintf($syslogMsgTpl, $quotahash{$skey}{'tally'}, $quotahash{$skey}{'quota'}, "OVER_QUOTA"); 298 | logger($syslogMsg); 299 | syslog(LOG_WARNING, $syslogMsg); 300 | return "471 $deltaconf message quota exceeded"; 301 | } 302 | $syslogMsg = sprintf($syslogMsgTpl, $quotahash{$skey}{'tally'}, $quotahash{$skey}{'quota'}, "UPDATE"); 303 | logger($syslogMsg); 304 | syslog(LOG_INFO, $syslogMsg); 305 | return "dunno"; 306 | } 307 | 308 | sub sigterm_handler { 309 | shutdown(SERVER,2); 310 | lock($lock); 311 | logger("SIGTERM received.\nFlushing cache...\nExiting."); 312 | &commit_cache; 313 | exit(0); 314 | } 315 | 316 | sub get_db_handler { 317 | my $dbh = DBI->connect($dsn, $db_user, $db_passwd, {PrintError => 0}); 318 | if (!defined($dbh)) { 319 | my $syslogMsg = sprintf("DB connection error (%s): %s", $DBI::err, $DBI::errstr); 320 | logger($syslogMsg); 321 | syslog(LOG_ERR, $syslogMsg); 322 | } 323 | return $dbh; 324 | } 325 | 326 | sub commit_cache { 327 | my $dbh = get_db_handler() 328 | or return undef; 329 | my $sql_query = $dbh->prepare($sql_updatequota); 330 | #lock($lock); -- lock at upper level 331 | while(($k,$v) = each(%quotahash)) { 332 | $sql_query->execute($quotahash{$k}{'sum'}, $quotahash{$k}{'expire'}, $k) 333 | or logger("Query error:".$sql_query->errstr); 334 | $quotahash{$k}{'sum'} = 0; 335 | } 336 | $dbh->disconnect; 337 | } 338 | 339 | sub flush_cache { 340 | lock($lock); 341 | foreach $k(keys %quotahash) { 342 | delete $quotahash{$k}; 343 | } 344 | } 345 | 346 | sub print_cache { 347 | foreach $k(keys %quotahash) { 348 | logger("$k: $quotahash{$k}{'quota'}, $quotahash{$k}{'tally'}"); 349 | } 350 | } 351 | 352 | # use this instead of daemonize if you're running the script with your own 353 | # daemon starter (e.g. start-stop-daemon) 354 | sub prepare_log { 355 | my ($i,$pid); 356 | my $mask = umask 0027; 357 | close STDIN; 358 | setsid(); 359 | close STDOUT; 360 | open STDIN, "/dev/null"; 361 | open LOG, ">>$LOGFILE" or die "Unable to open $LOGFILE: $!\n"; 362 | select((select(LOG), $|=1)[0]); 363 | open STDERR, ">>$LOGFILE" or die "Unable to redirect STDERR to STDOUT: $!\n"; 364 | umask $mask; 365 | } 366 | 367 | sub daemonize { 368 | my ($i,$pid); 369 | my $mask = umask 0027; 370 | print "SMTP Policy Daemon. Logging to $LOGFILE\n"; 371 | #Should i delete this?? 372 | #$ENV{PATH}="/bin:/usr/bin"; 373 | #chdir("/"); 374 | close STDIN; 375 | if (!defined(my $pid=fork())) { 376 | die "Impossible to fork\n"; 377 | } elsif ($pid >0) { 378 | exit 0; 379 | } 380 | setsid(); 381 | close STDOUT; 382 | open STDIN, "/dev/null"; 383 | open LOG, ">>$LOGFILE" or die "Unable to open $LOGFILE: $!\n"; 384 | select((select(LOG), $|=1)[0]); 385 | open STDERR, ">>$LOGFILE" or die "Unable to redirect STDERR to STDOUT: $!\n"; 386 | open PID, ">$PIDFILE" or die $!; 387 | print PID $$."\n"; 388 | close PID; 389 | umask $mask; 390 | } 391 | 392 | sub calcexpire { 393 | my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); 394 | my ($arg) = @_; 395 | if ($arg eq 'monthly') { 396 | $exp = mktime (0, 0, 0, 1, ++$mon, $year); 397 | } elsif ($arg eq 'weekly') { 398 | $exp = mktime (0, 0, 0, $mday+7-$wday, $mon, $year); 399 | } elsif ($arg eq 'daily') { 400 | $exp = mktime (0, 0, 0, ++$mday, $mon, $year); 401 | } elsif ($arg eq 'hourly') { 402 | $exp = mktime (0, $min, ++$hour, $mday, $mon, $year); 403 | } else { 404 | $exp = mktime (0, 0, 0, 1, ++$mon, $year); 405 | } 406 | return $exp; 407 | } 408 | 409 | sub logger { 410 | my ($arg) = @_; 411 | my $time = localtime(); 412 | chomp($time); 413 | print LOG "$time $arg\n"; 414 | } 415 | --------------------------------------------------------------------------------