#!/usr/bin/perl

# policyd-lemat2
# version 1.4
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#

# general purpose: limit the number of emails (recipients) an authenticated user can send
# to external recipients
# multiple emails to one recipient are counted as one

# requires: 

# 1. EVERY local user is authenticated. In eg. PHP mail() function sends mail 
# without authentication and therefore this policyd is ***not***able*** to limit it.

# 2. reject_sender_login_mismatch must be implemented because sending emails
# from local user to local user is NOT limited.

# 3. this script does not run as a daemon therefore may fail under heavy traffic.
# Your users should use port 587 or 465 only and this script should be run only on submission port.

# 4. postfix >= 2.2

# CREATE TABLE IF NOT EXISTS `lemat_limit` (
#  `username` varchar(255) NOT NULL,
#  `recipient` varchar(255) NOT NULL,
#  `data` datetime NOT NULL,
#  KEY `username` (`username`)
#) ENGINE=MyISAM DEFAULT CHARSET=utf8;

# master.cf

# policyd-lemat2 unix - n n - - spawn user=nobody argv=/usr/local/bin/policyd-lemat2

# submission inet n - n - - smtpd
# 	...
# 	smtpd_recipient_restrictions=reject_non_fqdn_recipient,reject_unknown_recipient_domain,lemat_limit,permit_sasl_authenticated,reject 

# and for smtps - if there is override smtpd_recipient_restrictions then you should put lemat_limit before permit_sasl_authenticated

# main.cf

# smtpd_restriction_classes=lemat_limit
# lemat_limit=check_policy_service unix:private/policyd-lemat2 

# smtpd_sender_login_maps = mysql:/etc/postfix/mysql_virtual_sender_login_maps.cfg
# smtpd_sender_restrictions =
#        reject_unknown_sender_domain,
#        reject_non_fqdn_sender,
#        reject_sender_login_mismatch,

# smtpd_recipient_restrictions =
#        ...
#        reject_non_fqdn_recipient,
#        reject_unknown_recipient_domain,
#        ...
#        lemat_limit,
#        ...
#        permit_sasl_authenticated
#        ...
#        reject_unauth_destination,
#        ...

# if you want cutsomize user limits you'll have to add two fields to a table 
# with user logins (postfixadmin table):

# ALTER TABLE `mailbox` ADD `soft_limit` INT NOT NULL ,
# ADD `hard_limit` INT NOT NULL ;

# or create your own table:

# CREATE TABLE IF NOT EXISTS `mailbox` (
#  `username` varchar(255) NOT NULL,  
#  `soft_limit` int(11) NOT NULL,   
#  `hard_limit` int(11) NOT NULL,
#  PRIMARY KEY (`username`)
# ) ENGINE=MyISAM;

use version; our $VERSION = qv('1.4');

use strict;

use IO::Handle;
use Sys::Syslog qw(:DEFAULT setlogsock);
use DBI;

my $VERBOSE = 0;

my $DEFAULT_RESPONSE = 'DUNNO';
my $ERROR_RESPONSE = 'DEFER_IF_PERMIT server configuration error';
#
# Syslogging options for verbose mode and for fatal errors.
# NOTE: comment out the $syslog_socktype line if syslogging does not
# work on your system.
#

my $syslog_socktype = 'unix'; # inet, unix, stream, console
my $syslog_facility = 'mail';
my $syslog_options  = 'pid';
my $syslog_ident    = 'postfix/policyd-lemat2';

# ----------------------------------------------------------
#                      initialization
# ----------------------------------------------------------

#
# Log an error and abort.
#
sub fatal_exit {
    syslog(err     => "fatal_exit: @_");
    syslog(warning => "fatal_exit: @_");
    syslog(info    => "fatal_exit: @_");
    die("fatal: @_");
}

#
# Unbuffer standard output.
#
STDOUT->autoflush(1);

# syslog so that people can actually see our messages.

setlogsock($syslog_socktype);
openlog($syslog_ident, $syslog_options, $syslog_facility);

my %attr;
while (<STDIN>) {
	chomp;
   
	if (/=/) {
		my ($key, $value) =split (/=/, $_, 2);
		$attr{$key} = $value;
		next;
		}
	elsif (length) {
		syslog(warning => sprintf("warning: ignoring garbage: %.100s", $_));
		next;
		}
   
	if ($VERBOSE) {
		for (sort keys %attr) {
			syslog(debug => "Attribute: %s=%s", $_, $attr{$_});
			}
		}

	# if user is authenticated
	if ($attr{'sasl_username'} ne '') {
		my($suser,$sdomain)=split (/@/, $attr{'sender'}, 2);
		my($ruser,$rdomain)=split (/@/, $attr{'recipient'}, 2);

		# if @localdomain -> @localdomain
		if ($sdomain eq $rdomain) {
			STDOUT->print("action=$DEFAULT_RESPONSE\n\n");
			}
		else {	# if @localdomain -> @externaldomain
#			my $action = do_query(%attr->{'sasl_username'},%attr->{'client_address'});
			my $action = do_query(%attr);
        		
			syslog(info => "%s: Policy action=%s", $attr{queue_id}, $action);
    
			STDOUT->print("action=$action\n\n");
			%attr = ();
			}
		}
	# mail from external
	else {
		STDOUT->print("action=$DEFAULT_RESPONSE\n\n");
		}
	}


sub do_query {

my $attr=$_[0];
my $user=$attr{'sasl_username'};
my $recipient=$attr{'recipient'};
my $client=$attr{'client_address'};

#my $user=$_[0];
#my $client=$_[1];

my $dbh = DBI->connect('DBI:mysql:postfix', 'postfix', 'haslo', { PrintError => 0 } );
if (!$dbh) {
        syslog(err => "Could not connect to database: $DBI::errstr");
        return $ERROR_RESPONSE;
        }

my $SOFT_LIMIT = 50;
my $HARD_LIMIT = 2*$SOFT_LIMIT;

# fetch config from database, you may comment this out if you don't have extra fields (limits per user)
my $sth = $dbh->prepare("SELECT * FROM mailbox WHERE username=?");
if (!$sth->execute($user)) {
        syslog(err => "Couldn't execute statement: " . $sth->errstr);
        return $ERROR_RESPONSE;
        }

if ( my $result = $sth->fetchrow_hashref() ) {
        if ($result->{soft_limit} > 0) {
                $SOFT_LIMIT=$result->{soft_limit};
                }
        if ($result->{hard_limit} > 0) {
                $HARD_LIMIT=$result->{hard_limit};
                }
        }

# insert into database
my $sth = $dbh->prepare("INSERT INTO lemat_limit (username,recipient,data) VALUES (?,?,now())");
if (!$sth->execute($user,$recipient)) {
        syslog(err => "Couldn't execute statement: " . $sth->errstr);
        return $ERROR_RESPONSE;
        }

# garbage collector
if (rand() > 0.9) {
        my $sth = $dbh->prepare("DELETE FROM lemat_limit WHERE DATE_SUB(now(),INTERVAL 7 DAY)>data");
        if (!$sth->execute()) {
                syslog(err => "Couldn't execute statement: " . $sth->errstr);
                return $ERROR_RESPONSE;
                }
        }

# select count() emails from database  
my $sth = $dbh->prepare("SELECT count(DISTINCT recipient) as ile FROM lemat_limit WHERE username=? AND recipient!=? AND DATE_SUB(now(),INTERVAL 1 DAY)<data");
if (!$sth->execute($user,$recipient)) {
        syslog(err => "Couldn't execute statement: " . $sth->errstr);
        return $ERROR_RESPONSE;
        }

if ( my $result = $sth->fetchrow_hashref() ) {
        if ($result->{ile} > $HARD_LIMIT ) {
                return '550 sendmail limit ('.$HARD_LIMIT.') exceeded for '.$user.' '.$attr{'client_name'}.'['.$attr{'client_address'}.']';
                }
        }

# select count() emails from database
my $sth = $dbh->prepare("SELECT count(DISTINCT recipient) as ile FROM lemat_limit WHERE username=? AND recipient!=? AND DATE_SUB(now(),INTERVAL 1 HOUR)<data");
if (!$sth->execute($user,$recipient)) {
        syslog(err => "Couldn't execute statement: " . $sth->errstr);
        return $ERROR_RESPONSE;
        }

if ( my $result = $sth->fetchrow_hashref() ) {
        if ($result->{ile} > $SOFT_LIMIT ) {  
                return '550 sendmail limit ('.$SOFT_LIMIT.') exceeded for '.$user.' '.$attr{'client_name'}.'['.$attr{'client_address'}.']';
                }
        }

return $DEFAULT_RESPONSE;
$dbh->disconnect();
}
