File: //etc/exim.pl
#!/usr/bin/perl
#VERSION=16
sub get_domain_owner
{
my ($domain) = @_;
my $username="";
open(DOMAINOWNERS,"/etc/virtual/domainowners");
while (<DOMAINOWNERS>)
{
$_ =~ s/\n//;
my ($dmn,$usr) = split(/: /, $_);
if ($dmn eq $domain)
{
close(DOMAINOWNERS);
return $usr;
}
}
close(DOMAINOWNERS);
return -1;
}
sub safe_name
{
my ($name) = @_;
if ($name =~ /\//)
{
return 0;
}
if ($name =~ /\\/)
{
return 0;
}
if ($name =~ /\|/)
{
return 0;
}
if ($name =~ /\.\./)
{
return 0;
}
return 1;
}
# hit_limit_user
# checks to see if a username has hit the send limit.
# returns:
# -1 for "there is no limit"
# 0 for "still under the limit"
# 1 for "at the limit"
# 2 for "over the limit"
sub hit_limit_user
{
my($username) = @_;
my $count = 0;
my $email_limit = 0;
if (!safe_name($username))
{
return 2;
}
if (open (LIMIT, "/etc/virtual/limit_$username"))
{
$email_limit = int(<LIMIT>);
close(LIMIT);
}
else
{
open (LIMIT, "/etc/virtual/limit");
$email_limit = int(<LIMIT>);
close(LIMIT);
}
if ($email_limit > 0)
{
#check this users limit
$count = (stat("/etc/virtual/usage/$username"))[7];
#this is their last email.
if ($count == $email_limit)
{
return 1;
}
if ($count > $email_limit)
{
return 2;
}
return 0;
}
return -1;
}
# hit_limit_email
# same idea as hit_limit_user, except we check the limits (if any) for per-email accounts.
sub hit_limit_email
{
my($user,$domain) = @_;
if (!safe_name($user) || !safe_name($domain))
{
return 2;
}
my $user_email_limit = 0;
if (open (LIMIT, "/etc/virtual/$domain/limit/$user"))
{
$user_email_limit = int(<LIMIT>);
close(LIMIT);
}
else
{
if (open (LIMIT, "/etc/virtual/user_limit"))
{
$user_email_limit = int(<LIMIT>);
close(LIMIT);
}
}
if ($user_email_limit > 0)
{
my $count = 0;
$count = (stat("/etc/virtual/$domain/usage/$user"))[7];
if ($count == $user_email_limit)
{
return 1;
}
if ($count > $user_email_limit)
{
return 2;
}
return 0;
}
return -1;
}
#smtpauth
#called by exim to verify if an smtp user is allowed to
#send email through the server
#possible success:
# user is in /etc/virtual/domain.com/passwd and password matches
# user is in /etc/passwd and password matches in /etc/shadow
sub smtpauth
{
$username = Exim::expand_string('$1');
$password = Exim::expand_string('$2');
$extra = Exim::expand_string('$3');
$domain = "";
$unixuser = 1;
#check for netscape that offsets the login/pass by one
if (length($extra) > 0 )
{
if ($username eq "" || $username eq $password)
{
$username = $password;
$password = $extra;
}
}
if (!safe_name($username))
{
Exim::log_write("SMTPAuth: Invalid username: $username");
return "no";
}
if ($username =~ /\@/)
{
$unixuser = 0;
($username,$domain) = split(/\@/, $username);
if ($domain eq "") { return "no"; }
}
if ($unixuser == 1)
{
#the username passed doesn't have a domain, so its a system account
$homepath = (getpwnam($username))[7];
if ($homepath eq "") { return 0; }
open(PASSFILE, "< $homepath/.shadow") || return "no";
$crypted_pass = <PASSFILE>;
close PASSFILE;
if ($crypted_pass eq crypt($password, $crypted_pass))
{
my $limit_check = hit_limit_user($username);
if ($limit_check > 1)
{
die("The email send limit for $username has been reached\n");
}
return "yes";
}
else { return "no"; }
}
else
{
#the username contain a domain, which is now in $domain.
#this is a pure virtual pop account.
open(PASSFILE, "< /etc/virtual/$domain/passwd") || return "no";
while (<PASSFILE>)
{
($test_user,$test_pass) = split(/:/,$_);
$test_pass =~ s/\n//g; #snip out the newline at the end
if ($test_user eq $username)
{
if ($test_pass eq crypt($password, $test_pass))
{
close PASSFILE;
my $domain_owner = get_domain_owner($domain);
if ($domain_owner != -1)
{
my $limit_check = hit_limit_user($domain_owner);
if ($limit_check > 1)
{
die("The email send limit for $domain_owner has been reached\n");
}
$limit_check = hit_limit_email($username, $domain);
if ($limit_check > 1)
{
die("The email send limit for $username\@${domain} has been reached\n");
}
}
return "yes";
}
}
}
close PASSFILE;
return "no";
}
return "no";
}
sub find_uid_apache
{
my ($work_path) = @_;
my @pw;
# $pwd will probably look like '/home/username/domains/domain.com/public_html'
# it may or may not use /home though. others are /usr/home, but it's ultimately
# specified in the /etc/passwd file. We *could* parse through it, but for efficiency
# reasons, we'll only check /home and /usr/home .. if they change it, they can
# manually adjust if needed.
@dirs = split(/\//, $work_path);
foreach $dir (@dirs)
{
# check the dir name for a valid user
# get the home dir for that user
# compare it with the first part of the work_path
if ( (@pw = getpwnam($dir)) )
{
if ($work_path =~/^$pw[7]/)
{
return $pw[2];
}
}
}
return -1;
}
sub find_uid_auth_id
{
# this will be passwed either
# 'username' or 'user@domain.com'
my ($auth_id) = @_;
my $unixuser = 1;
my $domain = "";
my $user = "";
my $username = $auth_id;
my @pw;
if (!safe_name($username))
{
Exim::log_write("find_uid_auth_id: Invalid username: $username");
return "-1";
}
if ($auth_id =~ /\@/)
{
$unixuser = 0;
($user,$domain) = split(/\@/, $auth_id);
if ($domain eq "") { return "-1"; }
}
if (!$unixuser)
{
# we need to take $domain and get the user from /etc/virtual/domainowners
# once we find it, set $username
my $u = get_domain_owner($domain);;
if ($u != -1)
{
$username = $u;
}
}
#log_str("username found from $auth_id: $username:\n");
if ( (@pw = getpwnam($username)) )
{
return $pw[2];
}
return -1;
}
sub find_uid_sender
{
my $sender_address = Exim::expand_string('$sender_address');
my ($user,$domain) = split(/\@/, $sender_address);
my $primary_hostname = Exim::expand_string('$primary_hostname');
if ( $domain eq $primary_hostname )
{
@pw = getpwnam($user);
return $pw[2];
}
my $username = get_domain_owner($domain);
if ( (@pw = getpwnam($username)) )
{
return $pw[2];
}
return -1;
}
sub find_uid
{
my $uid = Exim::expand_string('$originator_uid');
my $username = getpwuid($uid);
my $auth_id = Exim::expand_string('$authenticated_id');
my $work_path = $ENV{'PWD'};
if ($username eq "apache" || $username eq "nobody" || $username eq "webapps")
{
$uid = find_uid_apache($work_path);
if ($uid != -1) { return $uid; }
}
$uid = find_uid_auth_id($auth_id);
if ($uid != -1) { return $uid; }
# we don't want to rely on this, but it's all thats left.
return find_uid_sender;
}
sub uid_exempt
{
my ($uid) = @_;
if ($uid == 0) { return 1; }
my $name = getpwuid($uid);
if ($name eq "root") { return 1; }
if ($name eq "diradmin") { return 1; }
return 0;
}
#check_limits
#used to enforce limits for the number of emails sent
#by a user. It also logs the bandwidth of the data
#for received mail.
sub check_limits
{
#find the curent user
$uid = find_uid();
#log_str("Found uid: $uid\n");
if (uid_exempt($uid)) { return "yes"; }
my $name="";
#check this users limit
$name = getpwuid($uid);
if (!defined($name))
{
#possibly the sender-verify
$name = "unknown";
#return "yes";
}
my $count = 0;
my $email_limit = 0;
if (open (LIMIT, "/etc/virtual/limit_$name"))
{
$email_limit = int(<LIMIT>);
close(LIMIT);
}
else
{
open (LIMIT, "/etc/virtual/limit");
$email_limit = int(<LIMIT>);
close(LIMIT);
}
my $sender_address = Exim::expand_string('$sender_address');
my $authenticated_id = Exim::expand_string('$authenticated_id');
my $sender_host_address = Exim::expand_string('$sender_host_address');
my $mid = Exim::expand_string('$message_id');
my $message_size = Exim::expand_string('$message_size');
my $local_part = Exim::expand_string('$local_part');
my $domain = Exim::expand_string('$domain');
my $timestamp = time();
my $is_retry = 0;
if ($email_limit > 0)
{
#check this users limit
$count = (stat("/etc/virtual/usage/$name"))[7];
if ($count > $email_limit)
{
die("You ($name) have reached your daily email limit of $email_limit emails\n");
}
if ($mid ne "")
{
if (! -d "/etc/virtual/usage/${name}_ids")
{
mkdir("/etc/virtual/usage/${name}_ids", 0770);
}
my $mid_char = substr($mid, 0, 1);
if (! -d "/etc/virtual/usage/${name}_ids/$mid_char")
{
mkdir("/etc/virtual/usage/${name}_ids/$mid_char", 0770);
}
if (! -d "/etc/virtual/usage/${name}_ids/$mid_char/$mid")
{
mkdir("/etc/virtual/usage/${name}_ids/$mid_char/$mid", 0770);
}
my $dest_str = get_b64_string("$local_part-$domain");
my $id_file = "/etc/virtual/usage/${name}_ids/$mid_char/$mid/$dest_str";
if (-f $id_file)
{
$is_retry = 1;
}
else
{
open(IDF, ">>$id_file");
print IDF "log_time=$timestamp\n";
close(IDF);
chmod (0660, $id_file);
}
}
#this is their last email.
if (($count == $email_limit) && ($is_retry != 1))
{
#taddle on the dataskq
#note that the sender_address here is only the person who sent the last email
#it doesnt meant that they have sent all the spam
#this action=limit will trigger a check on usage/user.bytes, and DA will try and figure it out.
open(TQ, ">>/etc/virtual/mail_task.queue");
print TQ "action=limit&username=$name&count=$count&limit=$email_limit&email=$sender_address&authenticated_id=$authenticated_id&sender_host_address=$sender_host_address&log_time=$timestamp\n";
close(TQ);
chmod (0660, "/etc/virtual/mail_task.queue");
}
if ($is_retry != 1)
{
open(USAGE, ">>/etc/virtual/usage/$name");
print USAGE "1";
close(USAGE);
chmod (0660, "/etc/virtual/usage/$name");
}
}
if ( ($authenticated_id ne "") && ($is_retry != 1) )
{
my $user="";
my $domain="";
($user, $domain) = (split(/@/, $authenticated_id));
if (!safe_name($authenticated_id))
{
Exim::log_write("check_limits: Invalid username: $authenticated_id");
return "no";
}
if ($domain ne "")
{
my $user_email_limit = 0;
if (open (LIMIT, "/etc/virtual/$domain/limit/$user"))
{
$user_email_limit = int(<LIMIT>);
close(LIMIT);
}
else
{
if (open (LIMIT, "/etc/virtual/user_limit"))
{
$user_email_limit = int(<LIMIT>);
close(LIMIT);
}
}
if ($user_email_limit > 0)
{
$count = 0;
$count = (stat("/etc/virtual/$domain/usage/$user"))[7];
if ($count == $user_email_limit)
{
open(TQ, ">>/etc/virtual/mail_task.queue");
print TQ "action=userlimit&username=$name&count=$count&limit=$user_email_limit&email=$sender_address&authenticated_id=$authenticated_id&sender_host_address=$sender_host_address&log_time=$timestamp\n";
close(TQ);
chmod (0660, "/etc/virtual/mail_task.queue");
}
if ($count > $user_email_limit)
{
die("Your E-Mail ($authenticated_id) has reached it's daily email limit of $user_email_limit emails\n");
}
if (! -d "/etc/virtual/$domain/usage")
{
mkdir("/etc/virtual/$domain/usage", 0770);
}
if (-d "/etc/virtual/$domain/usage")
{
open(USAGE, ">>/etc/virtual/$domain/usage/$user");
print USAGE "1";
close(USAGE);
chmod (0660, "/etc/virtual/$domain/usage/$user");
}
}
}
}
log_bandwidth($uid,"type=email&email=$sender_address&method=outgoing&id=$mid&authenticated_id=$authenticated_id&sender_host_address=$sender_host_address&log_time=$timestamp&message_size=$message_size&local_part=$local_part&domain=$domain");
return "yes"
}
sub log_email
{
my($lp,$dmn) = @_;
#log_str("logging $lp\@$dmn\n");
my $user = get_domain_owner($dmn);
if ($user == -1) { return "no"; }
my $mid = Exim::expand_string('$message_id');
my $timestamp = time();
if ( (@pw = getpwnam($user)) )
{
log_bandwidth($pw[2],"type=email&email=$lp\@$dmn&method=incoming&log_time=$timestamp&id=$mid");
}
return "yes";
}
sub save_virtual_user
{
my $dmn = Exim::expand_string('$domain');
my $lp = Exim::expand_string('$local_part');
my $usr = "";
my $pss = "";
my $entry = "";
if (!safe_name($dmn) || !safe_name($lp))
{
Exim::log_write("save_virtual_user: Invalid username: $lp or domain: $dmn");
return "no";
}
open (PASSWD, "/etc/virtual/$dmn/passwd") || return "no";
while ($entry = <PASSWD>) {
($usr,$pss) = split(/:/,$entry);
if ($usr eq $lp) {
close(PASSWD);
log_email($lp, $dmn);
return "yes";
}
}
close (PASSWD);
return "no";
}
sub log_bandwidth
{
my ($uid,$data) = @_;
my $name = getpwuid($uid);
if (uid_exempt($uid)) { return; }
if ($name eq "") { $name = "unknown"; }
my $bytes = Exim::expand_string('$message_size');
if ($bytes == -1) { return; }
my $work_path = $ENV{'PWD'};
open (BYTES, ">>/etc/virtual/usage/$name.bytes");
print BYTES "$bytes=$data&path=$work_path\n";
close(BYTES);
chmod (0660, "/etc/virtual/usage/$name.bytes");
}
sub get_b64_string
{
my ($str) = @_;
eval
{
require MIME::Base64;
MIME::Base64->import();
};
unless($@)
{
my $enc = MIME::Base64::encode_base64($str);
# an evil newline is added. get rid of it.
$enc =~ s/\n//;
return $enc;
}
return $str;
}
sub log_str
{
my ($str) = @_;
open (LOG, ">> /tmp/test.txt");
print LOG $str;
close(LOG);
}